Merge branch 'master' into feature_machinesettings

This commit is contained in:
fieldOfView 2016-08-15 16:07:53 +02:00
commit 7d37cbd5d2
383 changed files with 295204 additions and 0 deletions

View file

@ -0,0 +1,127 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from UM.Mesh.MeshReader import MeshReader
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Logger import Logger
from UM.Math.Matrix import Matrix
from UM.Math.Vector import Vector
from UM.Scene.SceneNode import SceneNode
from UM.Scene.GroupDecorator import GroupDecorator
from UM.Math.Quaternion import Quaternion
from UM.Job import Job
import math
import zipfile
try:
import xml.etree.cElementTree as ET
except ImportError:
import xml.etree.ElementTree as ET
## Base implementation for reading 3MF files. Has no support for textures. Only loads meshes!
class ThreeMFReader(MeshReader):
def __init__(self):
super().__init__()
self._supported_extensions = [".3mf"]
self._namespaces = {
"3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
"cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
}
def read(self, file_name):
result = SceneNode()
# The base object of 3mf is a zipped archive.
archive = zipfile.ZipFile(file_name, "r")
try:
root = ET.parse(archive.open("3D/3dmodel.model"))
# There can be multiple objects, try to load all of them.
objects = root.findall("./3mf:resources/3mf:object", self._namespaces)
if len(objects) == 0:
Logger.log("w", "No objects found in 3MF file %s, either the file is corrupt or you are using an outdated format", file_name)
return None
for entry in objects:
mesh_builder = MeshBuilder()
node = SceneNode()
vertex_list = []
#for vertex in entry.mesh.vertices.vertex:
for vertex in entry.findall(".//3mf:vertex", self._namespaces):
vertex_list.append([vertex.get("x"), vertex.get("y"), vertex.get("z")])
Job.yieldThread()
triangles = entry.findall(".//3mf:triangle", self._namespaces)
mesh_builder.reserveFaceCount(len(triangles))
for triangle in triangles:
v1 = int(triangle.get("v1"))
v2 = int(triangle.get("v2"))
v3 = int(triangle.get("v3"))
mesh_builder.addFaceByPoints(vertex_list[v1][0], vertex_list[v1][1], vertex_list[v1][2],
vertex_list[v2][0], vertex_list[v2][1], vertex_list[v2][2],
vertex_list[v3][0], vertex_list[v3][1], vertex_list[v3][2])
Job.yieldThread()
# Rotate the model; We use a different coordinate frame.
rotation = Matrix()
rotation.setByRotationAxis(-0.5 * math.pi, Vector(1, 0, 0))
# TODO: We currently do not check for normals and simply recalculate them.
mesh_builder.calculateNormals()
mesh_builder.setFileName(file_name)
node.setMeshData(mesh_builder.build().getTransformed(rotation))
node.setSelectable(True)
transformations = root.findall("./3mf:build/3mf:item[@objectid='{0}']".format(entry.get("id")), self._namespaces)
transformation = transformations[0] if transformations else None
if transformation is not None and transformation.get("transform"):
splitted_transformation = transformation.get("transform").split()
## Transformation is saved as:
## M00 M01 M02 0.0
## M10 M11 M12 0.0
## M20 M21 M22 0.0
## M30 M31 M32 1.0
## We switch the row & cols as that is how everyone else uses matrices!
temp_mat = Matrix()
# Rotation & Scale
temp_mat._data[0,0] = splitted_transformation[0]
temp_mat._data[1,0] = splitted_transformation[1]
temp_mat._data[2,0] = splitted_transformation[2]
temp_mat._data[0,1] = splitted_transformation[3]
temp_mat._data[1,1] = splitted_transformation[4]
temp_mat._data[2,1] = splitted_transformation[5]
temp_mat._data[0,2] = splitted_transformation[6]
temp_mat._data[1,2] = splitted_transformation[7]
temp_mat._data[2,2] = splitted_transformation[8]
# Translation
temp_mat._data[0,3] = splitted_transformation[9]
temp_mat._data[1,3] = splitted_transformation[10]
temp_mat._data[2,3] = splitted_transformation[11]
node.setTransformation(temp_mat)
result.addChild(node)
Job.yieldThread()
# If there is more then one object, group them.
if len(objects) > 1:
group_decorator = GroupDecorator()
result.addDecorator(group_decorator)
elif len(objects) == 1:
result = result.getChildren()[0] # Only one object found, return that.
except Exception as e:
Logger.log("e", "exception occured in 3mf reader: %s", e)
try: # Selftest - There might be more functions that should fail
boundingBox = result.getBoundingBox()
boundingBox.isValid()
except:
return None
return result

View file

@ -0,0 +1,27 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from . import ThreeMFReader
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {
"plugin": {
"name": catalog.i18nc("@label", "3MF Reader"),
"author": "Ultimaker",
"version": "1.0",
"description": catalog.i18nc("@info:whatsthis", "Provides support for reading 3MF files."),
"api": 3
},
"mesh_reader": [
{
"extension": "3mf",
"description": catalog.i18nc("@item:inlistbox", "3MF File")
}
]
}
def register(app):
return { "mesh_reader": ThreeMFReader.ThreeMFReader() }

View file

@ -0,0 +1,54 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from PyQt5.QtCore import QTimer
from UM.Extension import Extension
from UM.Preferences import Preferences
from UM.Application import Application
from UM.Resources import Resources
from UM.Logger import Logger
class AutoSave(Extension):
def __init__(self):
super().__init__()
Preferences.getInstance().preferenceChanged.connect(self._triggerTimer)
self._global_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
Preferences.getInstance().addPreference("cura/autosave_delay", 1000 * 10)
self._change_timer = QTimer()
self._change_timer.setInterval(Preferences.getInstance().getValue("cura/autosave_delay"))
self._change_timer.setSingleShot(True)
self._change_timer.timeout.connect(self._onTimeout)
self._saving = False
def _triggerTimer(self, *args):
if not self._saving:
self._change_timer.start()
def _onGlobalStackChanged(self):
if self._global_stack:
self._global_stack.propertyChanged.disconnect(self._triggerTimer)
self._global_stack.containersChanged.disconnect(self._triggerTimer)
self._global_stack = Application.getInstance().getGlobalContainerStack()
if self._global_stack:
self._global_stack.propertyChanged.connect(self._triggerTimer)
self._global_stack.containersChanged.connect(self._triggerTimer)
def _onTimeout(self):
self._saving = True # To prevent the save process from triggering another autosave.
Logger.log("d", "Autosaving preferences, instances and profiles")
Application.getInstance().saveSettings()
Preferences.getInstance().writeToFile(Resources.getStoragePath(Resources.Preferences, Application.getInstance().getApplicationName() + ".cfg"))
self._saving = False

View file

@ -0,0 +1,21 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from . import AutoSave
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {
"plugin": {
"name": catalog.i18nc("@label", "Auto Save"),
"author": "Ultimaker",
"version": "1.0",
"description": catalog.i18nc("@info:whatsthis", "Automatically saves Preferences, Machines and Profiles after changes."),
"api": 3
},
}
def register(app):
return { "extension": AutoSave.AutoSave() }

View file

@ -0,0 +1,110 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from UM.i18n import i18nCatalog
from UM.Extension import Extension
from UM.Preferences import Preferences
from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
from UM.Version import Version
from PyQt5.QtQuick import QQuickView
from PyQt5.QtQml import QQmlComponent, QQmlContext
from PyQt5.QtCore import QUrl, pyqtSlot, QObject
import os.path
import collections
catalog = i18nCatalog("cura")
class ChangeLog(Extension, QObject,):
def __init__(self, parent = None):
QObject.__init__(self, parent)
Extension.__init__(self)
self._changelog_window = None
self._changelog_context = None
version_string = Application.getInstance().getVersion()
if version_string is not "master":
self._version = Version(version_string)
else:
self._version = None
self._change_logs = None
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
Preferences.getInstance().addPreference("general/latest_version_changelog_shown", "2.0.0") #First version of CURA with uranium
self.addMenuItem(catalog.i18nc("@item:inmenu", "Show Changelog"), self.showChangelog)
#self.showChangelog()
def getChangeLogs(self):
if not self._change_logs:
self.loadChangeLogs()
return self._change_logs
@pyqtSlot(result = str)
def getChangeLogString(self):
logs = self.getChangeLogs()
result = ""
for version in logs:
result += "<h1>" + str(version) + "</h1><br>"
result += ""
for change in logs[version]:
if str(change) != "":
result += "<b>" + str(change) + "</b><br>"
for line in logs[version][change]:
result += str(line) + "<br>"
result += "<br>"
pass
return result
def loadChangeLogs(self):
self._change_logs = collections.OrderedDict()
with open(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.txt"), "r",-1, "utf-8") as f:
open_version = None
open_header = "" # Initialise to an empty header in case there is no "*" in the first line of the changelog
for line in f:
line = line.replace("\n","")
if "[" in line and "]" in line:
line = line.replace("[","")
line = line.replace("]","")
open_version = Version(line)
open_header = ""
self._change_logs[open_version] = collections.OrderedDict()
elif line.startswith("*"):
open_header = line.replace("*","")
self._change_logs[open_version][open_header] = []
elif line != "":
if open_header not in self._change_logs[open_version]:
self._change_logs[open_version][open_header] = []
self._change_logs[open_version][open_header].append(line)
def _onEngineCreated(self):
if not self._version:
return #We're on dev branch.
if Preferences.getInstance().getValue("general/latest_version_changelog_shown") == "master":
latest_version_shown = Version("0.0.0")
else:
latest_version_shown = Version(Preferences.getInstance().getValue("general/latest_version_changelog_shown"))
if self._version > latest_version_shown:
self.showChangelog()
def showChangelog(self):
if not self._changelog_window:
self.createChangelogWindow()
self._changelog_window.show()
Preferences.getInstance().setValue("general/latest_version_changelog_shown", Application.getInstance().getVersion())
def hideChangelog(self):
if self._changelog_window:
self._changelog_window.hide()
def createChangelogWindow(self):
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.qml"))
component = QQmlComponent(Application.getInstance()._engine, path)
self._changelog_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._changelog_context.setContextProperty("manager", self)
self._changelog_window = component.create(self._changelog_context)

View file

@ -0,0 +1,43 @@
// Copyright (c) 2015 Ultimaker B.V.
// Cura is released under the terms of the AGPLv3 or higher.
import QtQuick 2.1
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
import UM 1.1 as UM
UM.Dialog
{
id: base
minimumWidth: 400 * Screen.devicePixelRatio
minimumHeight: 300 * Screen.devicePixelRatio
width: minimumWidth
height: minimumHeight
title: catalog.i18nc("@label", "Changelog")
ScrollView
{
width: parent.width
height: parent.height - 25
Label
{
text: manager.getChangeLogString()
width:base.width - 35
wrapMode: Text.Wrap;
}
}
Button
{
UM.I18nCatalog
{
id: catalog
name: "cura"
}
anchors.bottom:parent.bottom
text: catalog.i18nc("@action:button", "Close")
onClicked: base.hide()
anchors.horizontalCenter: parent.horizontalCenter
}
}

View file

@ -0,0 +1,92 @@
[2.1.3]
*Material Profiles
New material profiles for CPE+, PC, Nylon and TPU for the Ultimaker 2+ family.
[2.1.2]
Cura has been completely reengineered from the ground up for an even more seamless integration between hardware, software and materials. Together with its intuitive new user interface, its now also ready for any future developments. For the beginner Cura makes 3D printing incredibly easy, and for more advanced users, there are over 200 customizable settings.
*Select Multiple Objects
You now have the freedom to select and manipulate multiple objects at the same time.
*Grouping
You can now group objects together to make it easier to manipulate multiple objects.
*Undo/Redo
You can now undo and redo your actions, like moving an object or scaling.
*Setting Profiles
The new GUI allows custom profiles to load easily and intuitively, directly from Cura.
*3MF File Loading Support
Were happy to report we now support loading 3MF files. This is a new file format similar to AMF, but freely available.
*Intuitive Cut-Off Object Bottom
Weve added a feature that allows you to move objects below the build plate. You can either correct a model with a rough bottom, or print only a part of an object. Please note that the implementation differs greatly from the old one when it was just a setting.
*64-bit Windows Builds
An optimized 64-bit Windows Cura version is now available. This allows you to load larger model files.
*Automatic calculations
Cura allows you to set a number of lines/layers instead of millimeters. The engine automatically calculates the right settings.
*Per-Object Settings
Per-object settings allow you to override individual profile settings per object.
*Engine Features
*Line Width
Line width settings added per feature: Global, Walls, Top/Bottom, Infill, Skirt, Support.
*Pattern Settings
Pattern settings improved per feature: Top/Bottom, Infill, Support.
*Shell
*Alternate Skin Rotation
Helps to combat the pillowing problem on top layers.
*Alternate Extra Wall
For better infill adhesion.
*Horizontal Expansion
Allows to compensate model x,y-size to get a 1:1 result.
*Travel
*Avoid Printed Parts
When moving to the next part to print, avoid collisions between the nozzle and other parts which are already printed.
*Support
*Stair Step Height
Sets the balance between sturdy and hard to remove support. By setting steps of the stair-like bottom of the support resting on the model.
*ZigZag
A new, infill type thats easily breakable, introduced specially for support.
*Support Roofs
A new sub-feature to reduce scars the support leaves on overhangs.
*Support Towers
Specialized support for tiny overhang areas.
*Special Modes
*Surface Mode
This mode will print the surface of the mesh instead of the enclosed volume. This used to be called Only follow mesh surface. In addition to the surface mode and normal, a both mode has now been added. This ensures all closed volumes are printed as normal and all loose geometry as single walls.
*Experimental Features
*Conical Support
An experimental filament, cost-reduction feature, for support.
*Draft Shield
Prints a protective wall at a set distance around the object that prevents air from hitting the print, reducing warping.
*Fuzzy Skin
Prints the outer walls with a jittering motion to give your object a diffuse finish.
*Wire Printing
The object is printed with a mid-air / net-like structure, following the mesh surface. The build plate will move up and down during diagonal segments. Though not visible in layer view, you can view the result in other software, such as Repetier Host or http://chilipeppr.com/tinyg.

View file

@ -0,0 +1,21 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from . import ChangeLog
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {
"plugin": {
"name": catalog.i18nc("@label", "Changelog"),
"author": "Ultimaker",
"version": "1.0",
"description": catalog.i18nc("@info:whatsthis", "Shows changes since latest checked version."),
"api": 3
}
}
def register(app):
return {"extension": ChangeLog.ChangeLog()}

View file

@ -0,0 +1,123 @@
syntax = "proto3";
package cura.proto;
message ObjectList
{
repeated Object objects = 1;
repeated Setting settings = 2; // meshgroup settings (for one-at-a-time printing)
}
message Slice
{
repeated ObjectList object_lists = 1; // The meshgroups to be printed one after another
SettingList global_settings = 2; // The global settings used for the whole print job
repeated Extruder extruders = 3; // The settings sent to each extruder object
repeated SettingExtruder global_inherits_stack = 4; //From which stack the setting would inherit if not defined in a stack.
}
message Extruder
{
int32 id = 1;
SettingList settings = 2;
}
message Object
{
int64 id = 1;
bytes vertices = 2; //An array of 3 floats.
bytes normals = 3; //An array of 3 floats.
bytes indices = 4; //An array of ints.
repeated Setting settings = 5; // Setting override per object, overruling the global settings.
}
message Progress
{
float amount = 1;
}
message Layer {
int32 id = 1;
float height = 2; // Z position
float thickness = 3; // height of a single layer
repeated Polygon polygons = 4; // layer data
}
message Polygon {
enum Type {
NoneType = 0;
Inset0Type = 1;
InsetXType = 2;
SkinType = 3;
SupportType = 4;
SkirtType = 5;
InfillType = 6;
SupportInfillType = 7;
MoveCombingType = 8;
MoveRetractionType = 9;
}
Type type = 1; // Type of move
bytes points = 2; // The points of the polygon, or two points if only a line segment (Currently only line segments are used)
float line_width = 3; // The width of the line being laid down
}
message LayerOptimized {
int32 id = 1;
float height = 2; // Z position
float thickness = 3; // height of a single layer
repeated PathSegment path_segment = 4; // layer data
}
message PathSegment {
int32 extruder = 1; // The extruder used for this path segment
enum PointType {
Point2D = 0;
Point3D = 1;
}
PointType point_type = 2;
bytes points = 3; // The points defining the line segments, bytes of float[2/3] array of length N+1
bytes line_type = 4; // Type of line segment as an unsigned char array of length 1 or N, where N is the number of line segments in this path
bytes line_width = 5; // The widths of the line segments as bytes of a float array of length 1 or N
}
message GCodeLayer {
bytes data = 2;
}
message PrintTimeMaterialEstimates { // The print time for the whole print and material estimates for the extruder
float time = 1; // Total time estimate
repeated MaterialEstimates materialEstimates = 2; // materialEstimates data
}
message MaterialEstimates {
int64 id = 1;
float material_amount = 2; // material used in the extruder
}
message SettingList {
repeated Setting settings = 1;
}
message Setting {
string name = 1; // Internal key to signify a setting
bytes value = 2; // The value of the setting
}
message SettingExtruder {
string name = 1; //The setting key.
int32 extruder = 2; //From which extruder stack the setting should inherit.
}
message GCodePrefix {
bytes data = 2; //Header string to be prepended before the rest of the g-code sent from the engine.
}
message SlicingFinished {
}

View file

@ -0,0 +1,448 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from UM.Backend.Backend import Backend, BackendState
from UM.Application import Application
from UM.Scene.SceneNode import SceneNode
from UM.Preferences import Preferences
from UM.Signal import Signal
from UM.Logger import Logger
from UM.Message import Message
from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources
from UM.Settings.Validator import ValidatorState #To find if a setting is in an error state. We can't slice then.
from UM.Platform import Platform
import cura.Settings
from cura.OneAtATimeIterator import OneAtATimeIterator
from cura.Settings.ExtruderManager import ExtruderManager
from . import ProcessSlicedLayersJob
from . import ProcessGCodeJob
from . import StartSliceJob
import os
import sys
from time import time
from PyQt5.QtCore import QTimer
import Arcus
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class CuraEngineBackend(Backend):
## Starts the back-end plug-in.
#
# This registers all the signal listeners and prepares for communication
# with the back-end in general.
def __init__(self):
super().__init__()
# Find out where the engine is located, and how it is called.
# This depends on how Cura is packaged and which OS we are running on.
executable_name = "CuraEngine"
if Platform.isWindows():
executable_name += ".exe"
default_engine_location = executable_name
if os.path.exists(os.path.join(Application.getInstallPrefix(), "bin", executable_name)):
default_engine_location = os.path.join(Application.getInstallPrefix(), "bin", executable_name)
if hasattr(sys, "frozen"):
default_engine_location = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), executable_name)
if Platform.isLinux() and not default_engine_location:
if not os.getenv("PATH"):
raise OSError("There is something wrong with your Linux installation.")
for pathdir in os.getenv("PATH").split(os.pathsep):
execpath = os.path.join(pathdir, executable_name)
if os.path.exists(execpath):
default_engine_location = execpath
break
if not default_engine_location:
raise EnvironmentError("Could not find CuraEngine")
Logger.log("i", "Found CuraEngine at: %s" %(default_engine_location))
default_engine_location = os.path.abspath(default_engine_location)
Preferences.getInstance().addPreference("backend/location", default_engine_location)
self._scene = Application.getInstance().getController().getScene()
self._scene.sceneChanged.connect(self._onSceneChanged)
# Workaround to disable layer view processing if layer view is not active.
self._layer_view_active = False
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
self._onActiveViewChanged()
self._stored_layer_data = []
self._stored_optimized_layer_data = []
# Triggers for when to (re)start slicing:
self._global_container_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
self._active_extruder_stack = None
cura.Settings.ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderChanged)
self._onActiveExtruderChanged()
# When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired.
# This timer will group them up, and only slice for the last setting changed signal.
# TODO: Properly group propertyChanged signals by whether they are triggered by the same user interaction.
self._change_timer = QTimer()
self._change_timer.setInterval(500)
self._change_timer.setSingleShot(True)
self._change_timer.timeout.connect(self.slice)
# Listeners for receiving messages from the back-end.
self._message_handlers["cura.proto.Layer"] = self._onLayerMessage
self._message_handlers["cura.proto.LayerOptimized"] = self._onOptimizedLayerMessage
self._message_handlers["cura.proto.Progress"] = self._onProgressMessage
self._message_handlers["cura.proto.GCodeLayer"] = self._onGCodeLayerMessage
self._message_handlers["cura.proto.GCodePrefix"] = self._onGCodePrefixMessage
self._message_handlers["cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates
self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage
self._start_slice_job = None
self._slicing = False # Are we currently slicing?
self._restart = False # Back-end is currently restarting?
self._enabled = True # Should we be slicing? Slicing might be paused when, for instance, the user is dragging the mesh around.
self._always_restart = True # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness.
self._process_layers_job = None # The currently active job to process layers, or None if it is not processing layers.
self._backend_log_max_lines = 20000 # Maximum number of lines to buffer
self._error_message = None # Pop-up message that shows errors.
self.backendQuit.connect(self._onBackendQuit)
self.backendConnected.connect(self._onBackendConnected)
# When a tool operation is in progress, don't slice. So we need to listen for tool operations.
Application.getInstance().getController().toolOperationStarted.connect(self._onToolOperationStarted)
Application.getInstance().getController().toolOperationStopped.connect(self._onToolOperationStopped)
self._slice_start_time = None
## Called when closing the application.
#
# This function should terminate the engine process.
def close(self):
# Terminate CuraEngine if it is still running at this point
self._terminate()
super().close()
## Get the command that is used to call the engine.
# This is useful for debugging and used to actually start the engine.
# \return list of commands and args / parameters.
def getEngineCommand(self):
json_path = Resources.getPath(Resources.DefinitionContainers, "fdmprinter.def.json")
return [Preferences.getInstance().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", json_path, ""]
## Emitted when we get a message containing print duration and material amount.
# This also implies the slicing has finished.
# \param time The amount of time the print will take.
# \param material_amount The amount of material the print will use.
printDurationMessage = Signal()
## Emitted when the slicing process starts.
slicingStarted = Signal()
## Emitted when the slicing process is aborted forcefully.
slicingCancelled = Signal()
## Perform a slice of the scene.
def slice(self):
self._slice_start_time = time()
if not self._enabled or not self._global_container_stack: # We shouldn't be slicing.
# try again in a short time
self._change_timer.start()
return
self.printDurationMessage.emit(0, [0])
self._stored_layer_data = []
self._stored_optimized_layer_data = []
if self._slicing: # We were already slicing. Stop the old job.
self._terminate()
if self._process_layers_job: # We were processing layers. Stop that, the layers are going to change soon.
self._process_layers_job.abort()
self._process_layers_job = None
if self._error_message:
self._error_message.hide()
self.processingProgress.emit(0.0)
self.backendStateChange.emit(BackendState.NotStarted)
self._scene.gcode_list = []
self._slicing = True
self.slicingStarted.emit()
slice_message = self._socket.createMessage("cura.proto.Slice")
self._start_slice_job = StartSliceJob.StartSliceJob(slice_message)
self._start_slice_job.start()
self._start_slice_job.finished.connect(self._onStartSliceCompleted)
## Terminate the engine process.
def _terminate(self):
self._slicing = False
self._restart = True
self._stored_layer_data = []
self._stored_optimized_layer_data = []
if self._start_slice_job is not None:
self._start_slice_job.cancel()
self.slicingCancelled.emit()
self.processingProgress.emit(0)
Logger.log("d", "Attempting to kill the engine process")
if Application.getInstance().getCommandLineOption("external-backend", False):
self._createSocket()
return
if self._process is not None:
Logger.log("d", "Killing engine process")
try:
self._process.terminate()
Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait())
self._process = None
except Exception as e: # terminating a process that is already terminating causes an exception, silently ignore this.
Logger.log("d", "Exception occurred while trying to kill the engine %s", str(e))
## Event handler to call when the job to initiate the slicing process is
# completed.
#
# When the start slice job is successfully completed, it will be happily
# slicing. This function handles any errors that may occur during the
# bootstrapping of a slice job.
#
# \param job The start slice job that was just finished.
def _onStartSliceCompleted(self, job):
# Note that cancelled slice jobs can still call this method.
if self._start_slice_job is job:
self._start_slice_job = None
if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error:
return
if job.getResult() == StartSliceJob.StartJobResult.SettingError:
if Application.getInstance().getPlatformActivity:
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice. Please check your setting values for errors."))
self._error_message.show()
self.backendStateChange.emit(BackendState.Error)
else:
self.backendStateChange.emit(BackendState.NotStarted)
return
if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice:
if Application.getInstance().getPlatformActivity:
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice. No suitable models found."))
self._error_message.show()
self.backendStateChange.emit(BackendState.Error)
else:
self.backendStateChange.emit(BackendState.NotStarted)
return
# Preparation completed, send it to the backend.
self._socket.sendMessage(job.getSliceMessage())
Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time )
## Listener for when the scene has changed.
#
# This should start a slice if the scene is now ready to slice.
#
# \param source The scene node that was changed.
def _onSceneChanged(self, source):
if type(source) is not SceneNode:
return
if source is self._scene.getRoot():
return
if source.getMeshData() is None:
return
if source.getMeshData().getVertices() is None:
return
self._onChanged()
## Called when an error occurs in the socket connection towards the engine.
#
# \param error The exception that occurred.
def _onSocketError(self, error):
if Application.getInstance().isShuttingDown():
return
super()._onSocketError(error)
if error.getErrorCode() == Arcus.ErrorCode.Debug:
return
self._terminate()
if error.getErrorCode() not in [Arcus.ErrorCode.BindFailedError, Arcus.ErrorCode.ConnectionResetError, Arcus.ErrorCode.Debug]:
Logger.log("e", "A socket error caused the connection to be reset")
## A setting has changed, so check if we must reslice.
#
# \param instance The setting instance that has changed.
# \param property The property of the setting instance that has changed.
def _onSettingChanged(self, instance, property):
if property == "value": # Only reslice if the value has changed.
self._onChanged()
## Called when a sliced layer data message is received from the engine.
#
# \param message The protobuf message containing sliced layer data.
def _onLayerMessage(self, message):
self._stored_layer_data.append(message)
## Called when an optimized sliced layer data message is received from the engine.
#
# \param message The protobuf message containing sliced layer data.
def _onOptimizedLayerMessage(self, message):
self._stored_optimized_layer_data.append(message)
## Called when a progress message is received from the engine.
#
# \param message The protobuf message containing the slicing progress.
def _onProgressMessage(self, message):
self.processingProgress.emit(message.amount)
self.backendStateChange.emit(BackendState.Processing)
## Called when the engine sends a message that slicing is finished.
#
# \param message The protobuf message signalling that slicing is finished.
def _onSlicingFinishedMessage(self, message):
self.backendStateChange.emit(BackendState.Done)
self.processingProgress.emit(1.0)
self._slicing = False
Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time )
if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()):
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data)
self._process_layers_job.start()
self._stored_optimized_layer_data = []
## Called when a g-code message is received from the engine.
#
# \param message The protobuf message containing g-code, encoded as UTF-8.
def _onGCodeLayerMessage(self, message):
self._scene.gcode_list.append(message.data.decode("utf-8", "replace"))
## Called when a g-code prefix message is received from the engine.
#
# \param message The protobuf message containing the g-code prefix,
# encoded as UTF-8.
def _onGCodePrefixMessage(self, message):
self._scene.gcode_list.insert(0, message.data.decode("utf-8", "replace"))
## Called when a print time message is received from the engine.
#
# \param message The protobuff message containing the print time and
# material amount per extruder
def _onPrintTimeMaterialEstimates(self, message):
material_amounts = []
for index in range(message.repeatedMessageCount("materialEstimates")):
material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount)
self.printDurationMessage.emit(message.time, material_amounts)
## Creates a new socket connection.
def _createSocket(self):
super()._createSocket(os.path.abspath(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "Cura.proto")))
## Manually triggers a reslice
def forceSlice(self):
self._change_timer.start()
## Called when anything has changed to the stuff that needs to be sliced.
#
# This indicates that we should probably re-slice soon.
def _onChanged(self, *args, **kwargs):
self._change_timer.start()
## Called when the back-end connects to the front-end.
def _onBackendConnected(self):
if self._restart:
self._onChanged()
self._restart = False
## Called when the user starts using some tool.
#
# When the user starts using a tool, we should pause slicing to prevent
# continuously slicing while the user is dragging some tool handle.
#
# \param tool The tool that the user is using.
def _onToolOperationStarted(self, tool):
self._terminate() # Do not continue slicing once a tool has started
self._enabled = False # Do not reslice when a tool is doing it's 'thing'
## Called when the user stops using some tool.
#
# This indicates that we can safely start slicing again.
#
# \param tool The tool that the user was using.
def _onToolOperationStopped(self, tool):
self._enabled = True # Tool stop, start listening for changes again.
## Called when the user changes the active view mode.
def _onActiveViewChanged(self):
if Application.getInstance().getController().getActiveView():
view = Application.getInstance().getController().getActiveView()
if view.getPluginId() == "LayerView": # If switching to layer view, we should process the layers if that hasn't been done yet.
self._layer_view_active = True
# There is data and we're not slicing at the moment
# if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment.
if self._stored_optimized_layer_data and not self._slicing:
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data)
self._process_layers_job.start()
self._stored_optimized_layer_data = []
else:
self._layer_view_active = False
## Called when the back-end self-terminates.
#
# We should reset our state and start listening for new connections.
def _onBackendQuit(self):
if not self._restart and self._process:
Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait())
self._process = None
self._createSocket()
## Called when the global container stack changes
def _onGlobalStackChanged(self):
if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged)
self._global_container_stack.containersChanged.disconnect(self._onChanged)
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
if extruders:
for extruder in extruders:
extruder.propertyChanged.disconnect(self._onSettingChanged)
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
if self._global_container_stack:
self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed.
self._global_container_stack.containersChanged.connect(self._onChanged)
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
if extruders:
for extruder in extruders:
extruder.propertyChanged.connect(self._onSettingChanged)
self._onActiveExtruderChanged()
self._onChanged()
def _onActiveExtruderChanged(self):
if self._global_container_stack:
# Connect all extruders of the active machine. This might cause a few connects that have already happend,
# but that shouldn't cause issues as only new / unique connections are added.
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
if extruders:
for extruder in extruders:
extruder.propertyChanged.connect(self._onSettingChanged)
if self._active_extruder_stack:
self._active_extruder_stack.containersChanged.disconnect(self._onChanged)
self._active_extruder_stack = cura.Settings.ExtruderManager.getInstance().getActiveExtruderStack()
if self._active_extruder_stack:
self._active_extruder_stack.containersChanged.connect(self._onChanged)

View file

@ -0,0 +1,15 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from UM.Job import Job
from UM.Application import Application
class ProcessGCodeLayerJob(Job):
def __init__(self, message):
super().__init__()
self._scene = Application.getInstance().getController().getScene()
self._message = message
def run(self):
self._scene.gcode_list.append(self._message.data.decode("utf-8", "replace"))

View file

@ -0,0 +1,185 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from UM.Job import Job
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode
from UM.Application import Application
from UM.Mesh.MeshData import MeshData
from UM.Message import Message
from UM.i18n import i18nCatalog
from UM.Logger import Logger
from UM.Math.Vector import Vector
from cura import LayerDataBuilder
from cura import LayerDataDecorator
from cura import LayerPolygon
import numpy
from time import time
catalog = i18nCatalog("cura")
class ProcessSlicedLayersJob(Job):
def __init__(self, layers):
super().__init__()
self._layers = layers
self._scene = Application.getInstance().getController().getScene()
self._progress = None
self._abort_requested = False
## Aborts the processing of layers.
#
# This abort is made on a best-effort basis, meaning that the actual
# job thread will check once in a while to see whether an abort is
# requested and then stop processing by itself. There is no guarantee
# that the abort will stop the job any time soon or even at all.
def abort(self):
self._abort_requested = True
def run(self):
start_time = time()
if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView":
self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1)
self._progress.show()
Job.yieldThread()
if self._abort_requested:
if self._progress:
self._progress.hide()
return
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
new_node = SceneNode()
## Remove old layer data (if any)
for node in DepthFirstIterator(self._scene.getRoot()):
if node.callDecoration("getLayerData"):
node.getParent().removeChild(node)
break
if self._abort_requested:
if self._progress:
self._progress.hide()
return
mesh = MeshData()
layer_data = LayerDataBuilder.LayerDataBuilder()
layer_count = len(self._layers)
# Find the minimum layer number
# When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we
# instead simply offset all other layers so the lowest layer is always 0.
min_layer_number = 0
for layer in self._layers:
if layer.id < min_layer_number:
min_layer_number = layer.id
current_layer = 0
for layer in self._layers:
abs_layer_number = layer.id + abs(min_layer_number)
layer_data.addLayer(abs_layer_number)
this_layer = layer_data.getLayer(abs_layer_number)
layer_data.setLayerHeight(abs_layer_number, layer.height)
layer_data.setLayerThickness(abs_layer_number, layer.thickness)
for p in range(layer.repeatedMessageCount("path_segment")):
polygon = layer.getRepeatedMessage("path_segment", p)
extruder = polygon.extruder
line_types = numpy.fromstring(polygon.line_type, dtype="u1") # Convert bytearray to numpy array
line_types = line_types.reshape((-1,1))
points = numpy.fromstring(polygon.points, dtype="f4") # Convert bytearray to numpy array
if polygon.point_type == 0: # Point2D
points = points.reshape((-1,2)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
else: # Point3D
points = points.reshape((-1,3))
line_widths = numpy.fromstring(polygon.line_width, dtype="f4") # Convert bytearray to numpy array
line_widths = line_widths.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
# Create a new 3D-array, copy the 2D points over and insert the right height.
# This uses manual array creation + copy rather than numpy.insert since this is
# faster.
new_points = numpy.empty((len(points), 3), numpy.float32)
if polygon.point_type == 0: # Point2D
new_points[:, 0] = points[:, 0]
new_points[:, 1] = layer.height / 1000 # layer height value is in backend representation
new_points[:, 2] = -points[:, 1]
else: # Point3D
new_points[:, 0] = points[:, 0]
new_points[:, 1] = points[:, 2]
new_points[:, 2] = -points[:, 1]
this_poly = LayerPolygon.LayerPolygon(layer_data, extruder, line_types, new_points, line_widths)
this_poly.buildCache()
this_layer.polygons.append(this_poly)
Job.yieldThread()
Job.yieldThread()
current_layer += 1
progress = (current_layer / layer_count) * 99
# TODO: Rebuild the layer data mesh once the layer has been processed.
# This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh.
if self._abort_requested:
if self._progress:
self._progress.hide()
return
if self._progress:
self._progress.setProgress(progress)
# We are done processing all the layers we got from the engine, now create a mesh out of the data
layer_mesh = layer_data.build()
if self._abort_requested:
if self._progress:
self._progress.hide()
return
# Add LayerDataDecorator to scene node to indicate that the node has layer data
decorator = LayerDataDecorator.LayerDataDecorator()
decorator.setLayerData(layer_mesh)
new_node.addDecorator(decorator)
new_node.setMeshData(mesh)
# Set build volume as parent, the build volume can move as a result of raft settings.
# It makes sense to set the build volume as parent: the print is actually printed on it.
new_node_parent = Application.getInstance().getBuildVolume()
new_node.setParent(new_node_parent) # Note: After this we can no longer abort!
settings = Application.getInstance().getGlobalContainerStack()
if not settings.getProperty("machine_center_is_zero", "value"):
new_node.setPosition(Vector(-settings.getProperty("machine_width", "value") / 2, 0.0, settings.getProperty("machine_depth", "value") / 2))
if self._progress:
self._progress.setProgress(100)
view = Application.getInstance().getController().getActiveView()
if view.getPluginId() == "LayerView":
view.resetLayerData()
if self._progress:
self._progress.hide()
# Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed.
self._layers = None
Logger.log("d", "Processing layers took %s seconds", time() - start_time)
def _onActiveViewChanged(self):
if self.isRunning():
if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView":
if not self._progress:
self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0)
if self._progress.getProgress() != 100:
self._progress.show()
else:
if self._progress:
self._progress.hide()

View file

@ -0,0 +1,270 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
import numpy
from string import Formatter
from enum import IntEnum
from UM.Job import Job
from UM.Application import Application
from UM.Logger import Logger
from UM.Scene.SceneNode import SceneNode
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Settings.Validator import ValidatorState
from UM.Settings.SettingRelation import RelationType
from cura.OneAtATimeIterator import OneAtATimeIterator
import cura.Settings
class StartJobResult(IntEnum):
Finished = 1
Error = 2
SettingError = 3
NothingToSlice = 4
## Formatter class that handles token expansion in start/end gcod
class GcodeStartEndFormatter(Formatter):
def get_value(self, key, args, kwargs): # [CodeStyle: get_value is an overridden function from the Formatter class]
if isinstance(key, str):
try:
return kwargs[key]
except KeyError:
Logger.log("w", "Unable to replace '%s' placeholder in start/end gcode", key)
return "{" + key + "}"
else:
Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end gcode", key)
return "{" + str(key) + "}"
## Job class that builds up the message of scene data to send to CuraEngine.
class StartSliceJob(Job):
def __init__(self, slice_message):
super().__init__()
self._scene = Application.getInstance().getController().getScene()
self._slice_message = slice_message
self._is_cancelled = False
def getSliceMessage(self):
return self._slice_message
## Check if a stack has any errors.
## returns true if it has errors, false otherwise.
def _checkStackForErrors(self, stack):
if stack is None:
return False
for key in stack.getAllKeys():
validation_state = stack.getProperty(key, "validationState")
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
Logger.log("w", "Setting %s is not valid, but %s. Aborting slicing.", key, validation_state)
return True
Job.yieldThread()
return False
## Runs the job that initiates the slicing.
def run(self):
stack = Application.getInstance().getGlobalContainerStack()
if not stack:
self.setResult(StartJobResult.Error)
return
# Don't slice if there is a setting with an error value.
if not Application.getInstance().getMachineManager().isActiveStackValid:
self.setResult(StartJobResult.SettingError)
return
# Don't slice if there is a per object setting with an error value.
for node in DepthFirstIterator(self._scene.getRoot()):
if type(node) is not SceneNode or not node.isSelectable():
continue
if self._checkStackForErrors(node.callDecoration("getStack")):
self.setResult(StartJobResult.SettingError)
return
with self._scene.getSceneLock():
# Remove old layer data.
for node in DepthFirstIterator(self._scene.getRoot()):
if node.callDecoration("getLayerData"):
node.getParent().removeChild(node)
break
# Get the objects in their groups to print.
object_groups = []
if stack.getProperty("print_sequence", "value") == "one_at_a_time":
for node in OneAtATimeIterator(self._scene.getRoot()):
temp_list = []
# Node can't be printed, so don't bother sending it.
if getattr(node, "_outside_buildarea", False):
continue
children = node.getAllChildren()
children.append(node)
for child_node in children:
if type(child_node) is SceneNode and child_node.getMeshData() and child_node.getMeshData().getVertices() is not None:
temp_list.append(child_node)
if temp_list:
object_groups.append(temp_list)
Job.yieldThread()
if len(object_groups) == 0:
Logger.log("w", "No objects suitable for one at a time found, or no correct order found")
else:
temp_list = []
for node in DepthFirstIterator(self._scene.getRoot()):
if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
if not getattr(node, "_outside_buildarea", False):
temp_list.append(node)
Job.yieldThread()
if temp_list:
object_groups.append(temp_list)
# There are cases when there is nothing to slice. This can happen due to one at a time slicing not being
# able to find a possible sequence or because there are no objects on the build plate (or they are outside
# the build volume)
if not object_groups:
self.setResult(StartJobResult.NothingToSlice)
return
self._buildGlobalSettingsMessage(stack)
self._buildGlobalInheritsStackMessage(stack)
for extruder_stack in cura.Settings.ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
self._buildExtruderMessage(extruder_stack)
for group in object_groups:
group_message = self._slice_message.addRepeatedMessage("object_lists")
if group[0].getParent().callDecoration("isGroup"):
self._handlePerObjectSettings(group[0].getParent(), group_message)
for object in group:
mesh_data = object.getMeshData().getTransformed(object.getWorldTransformation())
obj = group_message.addRepeatedMessage("objects")
obj.id = id(object)
verts = numpy.array(mesh_data.getVertices())
# Convert from Y up axes to Z up axes. Equals a 90 degree rotation.
verts[:, [1, 2]] = verts[:, [2, 1]]
verts[:, 1] *= -1
obj.vertices = verts
self._handlePerObjectSettings(object, obj)
Job.yieldThread()
self.setResult(StartJobResult.Finished)
def cancel(self):
super().cancel()
self._is_cancelled = True
def isCancelled(self):
return self._is_cancelled
def _expandGcodeTokens(self, key, value, settings):
try:
# any setting can be used as a token
fmt = GcodeStartEndFormatter()
return str(fmt.format(value, **settings)).encode("utf-8")
except:
Logger.logException("w", "Unable to do token replacement on start/end gcode")
return str(value).encode("utf-8")
## Create extruder message from stack
def _buildExtruderMessage(self, stack):
message = self._slice_message.addRepeatedMessage("extruders")
message.id = int(stack.getMetaDataEntry("position"))
material_instance_container = stack.findContainer({"type": "material"})
for key in stack.getAllKeys():
setting = message.getMessage("settings").addRepeatedMessage("settings")
setting.name = key
if key == "material_guid" and material_instance_container:
# Also send the material GUID. This is a setting in fdmprinter, but we have no interface for it.
setting.value = str(material_instance_container.getMetaDataEntry("GUID", "")).encode("utf-8")
else:
setting.value = str(stack.getProperty(key, "value")).encode("utf-8")
Job.yieldThread()
## Sends all global settings to the engine.
#
# The settings are taken from the global stack. This does not include any
# per-extruder settings or per-object settings.
def _buildGlobalSettingsMessage(self, stack):
keys = stack.getAllKeys()
settings = {}
for key in keys:
settings[key] = stack.getProperty(key, "value")
start_gcode = settings["machine_start_gcode"]
settings["material_bed_temp_prepend"] = "{material_bed_temperature}" not in start_gcode #Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
settings["material_print_temp_prepend"] = "{material_print_temperature}" not in start_gcode
for key, value in settings.items(): #Add all submessages for each individual setting.
setting_message = self._slice_message.getMessage("global_settings").addRepeatedMessage("settings")
setting_message.name = key
if key == "machine_start_gcode" or key == "machine_end_gcode": #If it's a g-code message, use special formatting.
setting_message.value = self._expandGcodeTokens(key, value, settings)
else:
setting_message.value = str(value).encode("utf-8")
## Sends for some settings which extruder they should fallback to if not
# set.
#
# This is only set for settings that have the global_inherits_stack
# property.
#
# \param stack The global stack with all settings, from which to read the
# global_inherits_stack property.
def _buildGlobalInheritsStackMessage(self, stack):
for key in stack.getAllKeys():
extruder = int(round(float(stack.getProperty(key, "global_inherits_stack"))))
if extruder >= 0: #Set to a specific extruder.
setting_extruder = self._slice_message.addRepeatedMessage("global_inherits_stack")
setting_extruder.name = key
setting_extruder.extruder = extruder
## Check if a node has per object settings and ensure that they are set correctly in the message
# \param node \type{SceneNode} Node to check.
# \param message object_lists message to put the per object settings in
def _handlePerObjectSettings(self, node, message):
stack = node.callDecoration("getStack")
# Check if the node has a stack attached to it and the stack has any settings in the top container.
if stack:
# Check all settings for relations, so we can also calculate the correct values for dependant settings.
changed_setting_keys = set(stack.getTop().getAllKeys())
for key in stack.getTop().getAllKeys():
instance = stack.getTop().getInstance(key)
self._addRelations(changed_setting_keys, instance.definition.relations)
Job.yieldThread()
# Ensure that the engine is aware what the build extruder is
if stack.getProperty("machine_extruder_count", "value") > 1:
changed_setting_keys.add("extruder_nr")
# Get values for all changed settings
for key in changed_setting_keys:
setting = message.addRepeatedMessage("settings")
setting.name = key
setting.value = str(stack.getProperty(key, "value")).encode("utf-8")
Job.yieldThread()
## Recursive function to put all settings that require eachother for value changes in a list
# \param relations_set \type{set} Set of keys (strings) of settings that are influenced
# \param relations list of relation objects that need to be checked.
def _addRelations(self, relations_set, relations):
for relation in filter(lambda r: r.role == "value", relations):
if relation.type == RelationType.RequiresTarget:
continue
relations_set.add(relation.target.key)
self._addRelations(relations_set, relation.target.relations)

View file

@ -0,0 +1,22 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
#Shoopdawoop
from . import CuraEngineBackend
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {
"plugin": {
"name": catalog.i18nc("@label", "CuraEngine Backend"),
"author": "Ultimaker",
"description": catalog.i18nc("@info:whatsthis", "Provides the link to the CuraEngine slicing backend."),
"api": 3
}
}
def register(app):
return { "backend": CuraEngineBackend.CuraEngineBackend() }

View file

@ -0,0 +1,42 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
import os.path
from UM.Logger import Logger
from UM.Settings.InstanceContainer import InstanceContainer #The new profile to make.
from cura.ProfileReader import ProfileReader
## A plugin that reads profile data from Cura profile files.
#
# It reads a profile from a .curaprofile file, and returns it as a profile
# instance.
class CuraProfileReader(ProfileReader):
## Initialises the cura profile reader.
# This does nothing since the only other function is basically stateless.
def __init__(self):
super().__init__()
## Reads a cura profile from a file and returns it.
#
# \param file_name The file to read the cura profile from.
# \return The cura profile that was in the file, if any. If the file could
# not be read or didn't contain a valid profile, \code None \endcode is
# returned.
def read(self, file_name):
# Create an empty profile.
profile = InstanceContainer(os.path.basename(os.path.splitext(file_name)[0]))
profile.addMetaDataEntry("type", "quality")
try:
with open(file_name) as f: # Open file for reading.
serialized = f.read()
except IOError as e:
Logger.log("e", "Unable to open file %s for reading: %s", file_name, str(e))
return None
try:
profile.deserialize(serialized)
except Exception as e: # Parsing error. This is not a (valid) Cura profile then.
Logger.log("e", "Error while trying to parse profile: %s", str(e))
return None
return profile

View file

@ -0,0 +1,27 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from . import CuraProfileReader
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {
"plugin": {
"name": catalog.i18nc("@label", "Cura Profile Reader"),
"author": "Ultimaker",
"version": "1.0",
"description": catalog.i18nc("@info:whatsthis", "Provides support for importing Cura profiles."),
"api": 3
},
"profile_reader": [
{
"extension": "curaprofile",
"description": catalog.i18nc("@item:inlistbox", "Cura Profile")
}
]
}
def register(app):
return { "profile_reader": CuraProfileReader.CuraProfileReader() }

View file

@ -0,0 +1,26 @@
# Copyright (c) 2015 Ultimaker B.V.
# Copyright (c) 2013 David Braam
# Uranium is released under the terms of the AGPLv3 or higher.
from UM.Logger import Logger
from UM.SaveFile import SaveFile
from cura.ProfileWriter import ProfileWriter
## Writes profiles to Cura's own profile format with config files.
class CuraProfileWriter(ProfileWriter):
## Writes a profile to the specified file path.
#
# \param path \type{string} The file to output to.
# \param profile \type{Profile} The profile to write to that file.
# \return \code True \endcode if the writing was successful, or \code
# False \endcode if it wasn't.
def write(self, path, profile):
serialized = profile.serialize()
try:
with SaveFile(path, "wt", -1, "utf-8") as f: # Open the specified file.
f.write(serialized)
except Exception as e:
Logger.log("e", "Failed to write profile to %s: %s", path, str(e))
return False
return True

View file

@ -0,0 +1,27 @@
# Copyright (c) 2015 Ultimaker B.V.
# Uranium is released under the terms of the AGPLv3 or higher.
from . import CuraProfileWriter
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {
"plugin": {
"name": catalog.i18nc("@label", "Cura Profile Writer"),
"author": "Ultimaker",
"version": "1.0",
"description": catalog.i18nc("@info:whatsthis", "Provides support for exporting Cura profiles."),
"api": 3
},
"profile_writer": [
{
"extension": "curaprofile",
"description": catalog.i18nc("@item:inlistbox", "Cura Profile")
}
]
}
def register(app):
return { "profile_writer": CuraProfileWriter.CuraProfileWriter() }

View file

@ -0,0 +1,101 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
import re #Regular expressions for parsing escape characters in the settings.
import json
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Logger import Logger
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
from cura.ProfileReader import ProfileReader
## A class that reads profile data from g-code files.
#
# It reads the profile data from g-code files and stores it in a new profile.
# This class currently does not process the rest of the g-code in any way.
class GCodeProfileReader(ProfileReader):
## The file format version of the serialized g-code.
#
# It can only read settings with the same version as the version it was
# written with. If the file format is changed in a way that breaks reverse
# compatibility, increment this version number!
version = 3
## Dictionary that defines how characters are escaped when embedded in
# g-code.
#
# Note that the keys of this dictionary are regex strings. The values are
# not.
escape_characters = {
re.escape("\\\\"): "\\", #The escape character.
re.escape("\\n"): "\n", #Newlines. They break off the comment.
re.escape("\\r"): "\r" #Carriage return. Windows users may need this for visualisation in their editors.
}
## Initialises the g-code reader as a profile reader.
def __init__(self):
super().__init__()
## Reads a g-code file, loading the profile from it.
#
# \param file_name The name of the file to read the profile from.
# \return The profile that was in the specified file, if any. If the
# specified file was no g-code or contained no parsable profile, \code
# None \endcode is returned.
def read(self, file_name):
if file_name.split(".")[-1] != "gcode":
return None
prefix = ";SETTING_" + str(GCodeProfileReader.version) + " "
prefix_length = len(prefix)
# Loading all settings from the file.
# They are all at the end, but Python has no reverse seek any more since Python3.
# TODO: Consider moving settings to the start?
serialized = "" # Will be filled with the serialized profile.
try:
with open(file_name) as f:
for line in f:
if line.startswith(prefix):
# Remove the prefix and the newline from the line and add it to the rest.
serialized += line[prefix_length : -1]
except IOError as e:
Logger.log("e", "Unable to open file %s for reading: %s", file_name, str(e))
return None
serialized = unescapeGcodeComment(serialized)
Logger.log("i", "Serialized the following from %s: %s" %(file_name, repr(serialized)))
json_data = json.loads(serialized)
profile_strings = [json_data["global_quality"]]
profile_strings.extend(json_data.get("extruder_quality", []))
return [readQualityProfileFromString(profile_string) for profile_string in profile_strings]
## Unescape a string which has been escaped for use in a gcode comment.
#
# \param string The string to unescape.
# \return \type{str} The unscaped string.
def unescapeGcodeComment(string):
# Un-escape the serialized profile.
pattern = re.compile("|".join(GCodeProfileReader.escape_characters.keys()))
# Perform the replacement with a regular expression.
return pattern.sub(lambda m: GCodeProfileReader.escape_characters[re.escape(m.group(0))], string)
## Read in a profile from a serialized string.
#
# \param profile_string The profile data in serialized form.
# \return \type{Profile} the resulting Profile object or None if it could not be read.
def readQualityProfileFromString(profile_string):
# Create an empty profile - the id and name will be changed by the ContainerRegistry
profile = InstanceContainer("")
try:
profile.deserialize(profile_string)
except Exception as e: # Not a valid g-code file.
Logger.log("e", "Unable to serialise the profile: %s", str(e))
return None
return profile

View file

@ -0,0 +1,27 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from . import GCodeProfileReader
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {
"plugin": {
"name": catalog.i18nc("@label", "GCode Profile Reader"),
"author": "Ultimaker",
"version": "1.0",
"description": catalog.i18nc("@info:whatsthis", "Provides support for importing profiles from g-code files."),
"api": 3
},
"profile_reader": [
{
"extension": "gcode",
"description": catalog.i18nc("@item:inlistbox", "G-code File")
}
]
}
def register(app):
return { "profile_reader": GCodeProfileReader.GCodeProfileReader() }

View file

@ -0,0 +1,126 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from UM.Mesh.MeshWriter import MeshWriter
from UM.Logger import Logger
from UM.Application import Application
import UM.Settings.ContainerRegistry
from cura.CuraApplication import CuraApplication
from cura.Settings.ExtruderManager import ExtruderManager
from UM.Settings.InstanceContainer import InstanceContainer
import re #For escaping characters in the settings.
import json
## Writes g-code to a file.
#
# While this poses as a mesh writer, what this really does is take the g-code
# in the entire scene and write it to an output device. Since the g-code of a
# single mesh isn't separable from the rest what with rafts and travel moves
# and all, it doesn't make sense to write just a single mesh.
#
# So this plug-in takes the g-code that is stored in the root of the scene
# node tree, adds a bit of extra information about the profiles and writes
# that to the output device.
class GCodeWriter(MeshWriter):
## The file format version of the serialised g-code.
#
# It can only read settings with the same version as the version it was
# written with. If the file format is changed in a way that breaks reverse
# compatibility, increment this version number!
version = 3
## Dictionary that defines how characters are escaped when embedded in
# g-code.
#
# Note that the keys of this dictionary are regex strings. The values are
# not.
escape_characters = {
re.escape("\\"): "\\\\", # The escape character.
re.escape("\n"): "\\n", # Newlines. They break off the comment.
re.escape("\r"): "\\r" # Carriage return. Windows users may need this for visualisation in their editors.
}
def __init__(self):
super().__init__()
def write(self, stream, node, mode = MeshWriter.OutputMode.TextMode):
if mode != MeshWriter.OutputMode.TextMode:
Logger.log("e", "GCode Writer does not support non-text mode.")
return False
scene = Application.getInstance().getController().getScene()
gcode_list = getattr(scene, "gcode_list")
if gcode_list:
for gcode in gcode_list:
stream.write(gcode)
# Serialise the current container stack and put it at the end of the file.
settings = self._serialiseSettings(Application.getInstance().getGlobalContainerStack())
stream.write(settings)
return True
return False
## Create a new container with container 2 as base and container 1 written over it.
def _createFlattenedContainerInstance(self, instance_container1, instance_container2):
flat_container = InstanceContainer(instance_container2.getName())
flat_container.setDefinition(instance_container2.getDefinition())
flat_container.setMetaData(instance_container2.getMetaData())
for key in instance_container2.getAllKeys():
flat_container.setProperty(key, "value", instance_container2.getProperty(key, "value"))
for key in instance_container1.getAllKeys():
flat_container.setProperty(key, "value", instance_container1.getProperty(key, "value"))
return flat_container
## Serialises a container stack to prepare it for writing at the end of the
# g-code.
#
# The settings are serialised, and special characters (including newline)
# are escaped.
#
# \param settings A container stack to serialise.
# \return A serialised string of the settings.
def _serialiseSettings(self, stack):
prefix = ";SETTING_" + str(GCodeWriter.version) + " " # The prefix to put before each line.
prefix_length = len(prefix)
container_with_profile = stack.findContainer({"type": "quality"})
if not container_with_profile:
Logger.log("e", "No valid quality profile found, not writing settings to GCode!")
return ""
flat_global_container = self._createFlattenedContainerInstance(stack.getTop(),container_with_profile)
serialized = flat_global_container.serialize()
data = {"global_quality": serialized}
for extruder in ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
extruder_quality = extruder.findContainer({"type": "quality"})
if not extruder_quality:
Logger.log("w", "No extruder quality profile found, not writing quality for extruder %s to file!", extruder.getId())
continue
flat_extruder_quality = self._createFlattenedContainerInstance(extruder.getTop(), extruder_quality)
extruder_serialized = flat_extruder_quality.serialize()
data.setdefault("extruder_quality", []).append(extruder_serialized)
json_string = json.dumps(data)
# Escape characters that have a special meaning in g-code comments.
pattern = re.compile("|".join(GCodeWriter.escape_characters.keys()))
# Perform the replacement with a regular expression.
escaped_string = pattern.sub(lambda m: GCodeWriter.escape_characters[re.escape(m.group(0))], json_string)
# Introduce line breaks so that each comment is no longer than 80 characters. Prepend each line with the prefix.
result = ""
# Lines have 80 characters, so the payload of each line is 80 - prefix.
for pos in range(0, len(escaped_string), 80 - prefix_length):
result += prefix + escaped_string[pos : pos + 80 - prefix_length] + "\n"
return result

View file

@ -0,0 +1,30 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from . import GCodeWriter
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {
"plugin": {
"name": catalog.i18nc("@label", "GCode Writer"),
"author": "Ultimaker",
"version": "1.0",
"description": catalog.i18nc("@info:whatsthis", "Writes GCode to a file."),
"api": 3
},
"mesh_writer": {
"output": [{
"extension": "gcode",
"description": catalog.i18nc("@item:inlistbox", "GCode File"),
"mime_type": "text/x-gcode",
"mode": GCodeWriter.GCodeWriter.OutputMode.TextMode
}]
}
}
def register(app):
return { "mesh_writer": GCodeWriter.GCodeWriter() }

View file

@ -0,0 +1,196 @@
// Copyright (c) 2015 Ultimaker B.V.
// Cura is released under the terms of the AGPLv3 or higher.
import QtQuick 2.1
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
import UM 1.1 as UM
UM.Dialog
{
width: 350 * Screen.devicePixelRatio;
minimumWidth: 350 * Screen.devicePixelRatio;
maximumWidth: 350 * Screen.devicePixelRatio;
height: 250 * Screen.devicePixelRatio;
minimumHeight: 250 * Screen.devicePixelRatio;
maximumHeight: 250 * Screen.devicePixelRatio;
title: catalog.i18nc("@title:window", "Convert Image...")
GridLayout
{
UM.I18nCatalog{id: catalog; name:"cura"}
anchors.fill: parent;
Layout.fillWidth: true
columnSpacing: 16
rowSpacing: 4
columns: 1
UM.TooltipArea {
Layout.fillWidth:true
height: childrenRect.height
text: catalog.i18nc("@info:tooltip","The maximum distance of each pixel from \"Base.\"")
Row {
width: parent.width
Label {
text: catalog.i18nc("@action:label","Height (mm)")
width: 150
anchors.verticalCenter: parent.verticalCenter
}
TextField {
id: peak_height
objectName: "Peak_Height"
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: -500; top: 500;}
width: 180
onTextChanged: { manager.onPeakHeightChanged(text) }
}
}
}
UM.TooltipArea {
Layout.fillWidth:true
height: childrenRect.height
text: catalog.i18nc("@info:tooltip","The base height from the build plate in millimeters.")
Row {
width: parent.width
Label {
text: catalog.i18nc("@action:label","Base (mm)")
width: 150
anchors.verticalCenter: parent.verticalCenter
}
TextField {
id: base_height
objectName: "Base_Height"
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 0; top: 500;}
width: 180
onTextChanged: { manager.onBaseHeightChanged(text) }
}
}
}
UM.TooltipArea {
Layout.fillWidth:true
height: childrenRect.height
text: catalog.i18nc("@info:tooltip","The width in millimeters on the build plate.")
Row {
width: parent.width
Label {
text: catalog.i18nc("@action:label","Width (mm)")
width: 150
anchors.verticalCenter: parent.verticalCenter
}
TextField {
id: width
objectName: "Width"
focus: true
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 1; top: 500;}
width: 180
onTextChanged: { manager.onWidthChanged(text) }
}
}
}
UM.TooltipArea {
Layout.fillWidth:true
height: childrenRect.height
text: catalog.i18nc("@info:tooltip","The depth in millimeters on the build plate")
Row {
width: parent.width
Label {
text: catalog.i18nc("@action:label","Depth (mm)")
width: 150
anchors.verticalCenter: parent.verticalCenter
}
TextField {
id: depth
objectName: "Depth"
focus: true
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 1; top: 500;}
width: 180
onTextChanged: { manager.onDepthChanged(text) }
}
}
}
UM.TooltipArea {
Layout.fillWidth:true
height: childrenRect.height
text: catalog.i18nc("@info:tooltip","By default, white pixels represent high points on the mesh and black pixels represent low points on the mesh. Change this option to reverse the behavior such that black pixels represent high points on the mesh and white pixels represent low points on the mesh.")
Row {
width: parent.width
//Empty label so 2 column layout works.
Label {
text: ""
width: 150
anchors.verticalCenter: parent.verticalCenter
}
ComboBox {
id: image_color_invert
objectName: "Image_Color_Invert"
model: [ catalog.i18nc("@item:inlistbox","Lighter is higher"), catalog.i18nc("@item:inlistbox","Darker is higher") ]
width: 180
onCurrentIndexChanged: { manager.onImageColorInvertChanged(currentIndex) }
}
}
}
UM.TooltipArea {
Layout.fillWidth:true
height: childrenRect.height
text: catalog.i18nc("@info:tooltip","The amount of smoothing to apply to the image.")
Row {
width: parent.width
Label {
text: catalog.i18nc("@action:label","Smoothing")
width: 150
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: 180
height: 20
Layout.fillWidth:true
color: "transparent"
Slider {
id: smoothing
objectName: "Smoothing"
maximumValue: 100.0
stepSize: 1.0
width: 180
onValueChanged: { manager.onSmoothingChanged(value) }
}
}
}
}
}
rightButtons: [
Button
{
id:ok_button
text: catalog.i18nc("@action:button","OK");
onClicked: { manager.onOkButtonClicked() }
enabled: true
},
Button
{
id:cancel_button
text: catalog.i18nc("@action:button","Cancel");
onClicked: { manager.onCancelButtonClicked() }
enabled: true
}
]
}

View file

@ -0,0 +1,213 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
import numpy
from PyQt5.QtGui import QImage, qRed, qGreen, qBlue
from PyQt5.QtCore import Qt
from UM.Mesh.MeshReader import MeshReader
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Scene.SceneNode import SceneNode
from UM.Math.Vector import Vector
from UM.Job import Job
from UM.Logger import Logger
from .ImageReaderUI import ImageReaderUI
class ImageReader(MeshReader):
def __init__(self):
super(ImageReader, self).__init__()
self._supported_extensions = [".jpg", ".jpeg", ".bmp", ".gif", ".png"]
self._ui = ImageReaderUI(self)
def preRead(self, file_name):
img = QImage(file_name)
if img.isNull():
Logger.log("e", "Image is corrupt.")
return MeshReader.PreReadResult.failed
width = img.width()
depth = img.height()
largest = max(width, depth)
width = width / largest * self._ui.default_width
depth = depth / largest * self._ui.default_depth
self._ui.setWidthAndDepth(width, depth)
self._ui.showConfigUI()
self._ui.waitForUIToClose()
if self._ui.getCancelled():
return MeshReader.PreReadResult.cancelled
return MeshReader.PreReadResult.accepted
def read(self, file_name):
size = max(self._ui.getWidth(), self._ui.getDepth())
return self._generateSceneNode(file_name, size, self._ui.peak_height, self._ui.base_height, self._ui.smoothing, 512, self._ui.image_color_invert)
def _generateSceneNode(self, file_name, xz_size, peak_height, base_height, blur_iterations, max_size, image_color_invert):
scene_node = SceneNode()
mesh = MeshBuilder()
img = QImage(file_name)
if img.isNull():
Logger.log("e", "Image is corrupt.")
return None
width = max(img.width(), 2)
height = max(img.height(), 2)
aspect = height / width
if img.width() < 2 or img.height() < 2:
img = img.scaled(width, height, Qt.IgnoreAspectRatio)
base_height = max(base_height, 0)
peak_height = max(peak_height, -base_height)
xz_size = max(xz_size, 1)
scale_vector = Vector(xz_size, peak_height, xz_size)
if width > height:
scale_vector = scale_vector.set(z=scale_vector.z * aspect)
elif height > width:
scale_vector = scale_vector.set(x=scale_vector.x / aspect)
if width > max_size or height > max_size:
scale_factor = max_size / width
if height > width:
scale_factor = max_size / height
width = int(max(round(width * scale_factor), 2))
height = int(max(round(height * scale_factor), 2))
img = img.scaled(width, height, Qt.IgnoreAspectRatio)
width_minus_one = width - 1
height_minus_one = height - 1
Job.yieldThread()
texel_width = 1.0 / (width_minus_one) * scale_vector.x
texel_height = 1.0 / (height_minus_one) * scale_vector.z
height_data = numpy.zeros((height, width), dtype=numpy.float32)
for x in range(0, width):
for y in range(0, height):
qrgb = img.pixel(x, y)
avg = float(qRed(qrgb) + qGreen(qrgb) + qBlue(qrgb)) / (3 * 255)
height_data[y, x] = avg
Job.yieldThread()
if image_color_invert:
height_data = 1 - height_data
for _ in range(0, blur_iterations):
copy = numpy.pad(height_data, ((1, 1), (1, 1)), mode= "edge")
height_data += copy[1:-1, 2:]
height_data += copy[1:-1, :-2]
height_data += copy[2:, 1:-1]
height_data += copy[:-2, 1:-1]
height_data += copy[2:, 2:]
height_data += copy[:-2, 2:]
height_data += copy[2:, :-2]
height_data += copy[:-2, :-2]
height_data /= 9
Job.yieldThread()
height_data *= scale_vector.y
height_data += base_height
heightmap_face_count = 2 * height_minus_one * width_minus_one
total_face_count = heightmap_face_count + (width_minus_one * 2) * (height_minus_one * 2) + 2
mesh.reserveFaceCount(total_face_count)
# initialize to texel space vertex offsets.
# 6 is for 6 vertices for each texel quad.
heightmap_vertices = numpy.zeros((width_minus_one * height_minus_one, 6, 3), dtype = numpy.float32)
heightmap_vertices = heightmap_vertices + numpy.array([[
[0, base_height, 0],
[0, base_height, texel_height],
[texel_width, base_height, texel_height],
[texel_width, base_height, texel_height],
[texel_width, base_height, 0],
[0, base_height, 0]
]], dtype = numpy.float32)
offsetsz, offsetsx = numpy.mgrid[0: height_minus_one, 0: width - 1]
offsetsx = numpy.array(offsetsx, numpy.float32).reshape(-1, 1) * texel_width
offsetsz = numpy.array(offsetsz, numpy.float32).reshape(-1, 1) * texel_height
# offsets for each texel quad
heightmap_vertex_offsets = numpy.concatenate([offsetsx, numpy.zeros((offsetsx.shape[0], offsetsx.shape[1]), dtype=numpy.float32), offsetsz], 1)
heightmap_vertices += heightmap_vertex_offsets.repeat(6, 0).reshape(-1, 6, 3)
# apply height data to y values
heightmap_vertices[:, 0, 1] = heightmap_vertices[:, 5, 1] = height_data[:-1, :-1].reshape(-1)
heightmap_vertices[:, 1, 1] = height_data[1:, :-1].reshape(-1)
heightmap_vertices[:, 2, 1] = heightmap_vertices[:, 3, 1] = height_data[1:, 1:].reshape(-1)
heightmap_vertices[:, 4, 1] = height_data[:-1, 1:].reshape(-1)
heightmap_indices = numpy.array(numpy.mgrid[0:heightmap_face_count * 3], dtype=numpy.int32).reshape(-1, 3)
mesh._vertices[0:(heightmap_vertices.size // 3), :] = heightmap_vertices.reshape(-1, 3)
mesh._indices[0:(heightmap_indices.size // 3), :] = heightmap_indices
mesh._vertex_count = heightmap_vertices.size // 3
mesh._face_count = heightmap_indices.size // 3
geo_width = width_minus_one * texel_width
geo_height = height_minus_one * texel_height
# bottom
mesh.addFaceByPoints(0, 0, 0, 0, 0, geo_height, geo_width, 0, geo_height)
mesh.addFaceByPoints(geo_width, 0, geo_height, geo_width, 0, 0, 0, 0, 0)
# north and south walls
for n in range(0, width_minus_one):
x = n * texel_width
nx = (n + 1) * texel_width
hn0 = height_data[0, n]
hn1 = height_data[0, n + 1]
hs0 = height_data[height_minus_one, n]
hs1 = height_data[height_minus_one, n + 1]
mesh.addFaceByPoints(x, 0, 0, nx, 0, 0, nx, hn1, 0)
mesh.addFaceByPoints(nx, hn1, 0, x, hn0, 0, x, 0, 0)
mesh.addFaceByPoints(x, 0, geo_height, nx, 0, geo_height, nx, hs1, geo_height)
mesh.addFaceByPoints(nx, hs1, geo_height, x, hs0, geo_height, x, 0, geo_height)
# west and east walls
for n in range(0, height_minus_one):
y = n * texel_height
ny = (n + 1) * texel_height
hw0 = height_data[n, 0]
hw1 = height_data[n + 1, 0]
he0 = height_data[n, width_minus_one]
he1 = height_data[n + 1, width_minus_one]
mesh.addFaceByPoints(0, 0, y, 0, 0, ny, 0, hw1, ny)
mesh.addFaceByPoints(0, hw1, ny, 0, hw0, y, 0, 0, y)
mesh.addFaceByPoints(geo_width, 0, y, geo_width, 0, ny, geo_width, he1, ny)
mesh.addFaceByPoints(geo_width, he1, ny, geo_width, he0, y, geo_width, 0, y)
mesh.calculateNormals(fast=True)
scene_node.setMeshData(mesh.build())
return scene_node

View file

@ -0,0 +1,155 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
import os
import threading
from PyQt5.QtCore import Qt, QUrl, pyqtSignal, pyqtSlot, QObject
from PyQt5.QtQml import QQmlComponent, QQmlContext
from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
from UM.Logger import Logger
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class ImageReaderUI(QObject):
show_config_ui_trigger = pyqtSignal()
def __init__(self, image_reader):
super(ImageReaderUI, self).__init__()
self.image_reader = image_reader
self._ui_view = None
self.show_config_ui_trigger.connect(self._actualShowConfigUI)
self.default_width = 120
self.default_depth = 120
self._aspect = 1
self._width = self.default_width
self._depth = self.default_depth
self.base_height = 1
self.peak_height = 10
self.smoothing = 1
self.image_color_invert = False;
self._ui_lock = threading.Lock()
self._cancelled = False
self._disable_size_callbacks = False
def setWidthAndDepth(self, width, depth):
self._aspect = width / depth
self._width = width
self._depth = depth
def getWidth(self):
return self._width
def getDepth(self):
return self._depth
def getCancelled(self):
return self._cancelled
def waitForUIToClose(self):
self._ui_lock.acquire()
self._ui_lock.release()
def showConfigUI(self):
self._ui_lock.acquire()
self._cancelled = False
self.show_config_ui_trigger.emit()
def _actualShowConfigUI(self):
self._disable_size_callbacks = True
if self._ui_view is None:
self._createConfigUI()
self._ui_view.show()
self._ui_view.findChild(QObject, "Width").setProperty("text", str(self._width))
self._ui_view.findChild(QObject, "Depth").setProperty("text", str(self._depth))
self._disable_size_callbacks = False
self._ui_view.findChild(QObject, "Base_Height").setProperty("text", str(self.base_height))
self._ui_view.findChild(QObject, "Peak_Height").setProperty("text", str(self.peak_height))
self._ui_view.findChild(QObject, "Smoothing").setProperty("value", self.smoothing)
def _createConfigUI(self):
if self._ui_view is None:
Logger.log("d", "Creating ImageReader config UI")
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("ImageReader"), "ConfigUI.qml"))
component = QQmlComponent(Application.getInstance()._engine, path)
self._ui_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._ui_context.setContextProperty("manager", self)
self._ui_view = component.create(self._ui_context)
self._ui_view.setFlags(self._ui_view.flags() & ~Qt.WindowCloseButtonHint & ~Qt.WindowMinimizeButtonHint & ~Qt.WindowMaximizeButtonHint);
self._disable_size_callbacks = False
@pyqtSlot()
def onOkButtonClicked(self):
self._cancelled = False
self._ui_view.close()
self._ui_lock.release()
@pyqtSlot()
def onCancelButtonClicked(self):
self._cancelled = True
self._ui_view.close()
self._ui_lock.release()
@pyqtSlot(str)
def onWidthChanged(self, value):
if self._ui_view and not self._disable_size_callbacks:
if len(value) > 0:
self._width = float(value)
else:
self._width = 0
self._depth = self._width / self._aspect
self._disable_size_callbacks = True
self._ui_view.findChild(QObject, "Depth").setProperty("text", str(self._depth))
self._disable_size_callbacks = False
@pyqtSlot(str)
def onDepthChanged(self, value):
if self._ui_view and not self._disable_size_callbacks:
if len(value) > 0:
self._depth = float(value)
else:
self._depth = 0
self._width = self._depth * self._aspect
self._disable_size_callbacks = True
self._ui_view.findChild(QObject, "Width").setProperty("text", str(self._width))
self._disable_size_callbacks = False
@pyqtSlot(str)
def onBaseHeightChanged(self, value):
if (len(value) > 0):
self.base_height = float(value)
else:
self.base_height = 0
@pyqtSlot(str)
def onPeakHeightChanged(self, value):
if (len(value) > 0):
self.peak_height = float(value)
else:
self.peak_height = 0
@pyqtSlot(float)
def onSmoothingChanged(self, value):
self.smoothing = int(value)
@pyqtSlot(int)
def onImageColorInvertChanged(self, value):
if (value == 1):
self.image_color_invert = True
else:
self.image_color_invert = False

View file

@ -0,0 +1,43 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from . import ImageReader
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
def getMetaData():
return {
"plugin": {
"name": i18n_catalog.i18nc("@label", "Image Reader"),
"author": "Ultimaker",
"version": "1.0",
"description": i18n_catalog.i18nc("@info:whatsthis", "Enables ability to generate printable geometry from 2D image files."),
"api": 3
},
"mesh_reader": [
{
"extension": "jpg",
"description": i18n_catalog.i18nc("@item:inlistbox", "JPG Image")
},
{
"extension": "jpeg",
"description": i18n_catalog.i18nc("@item:inlistbox", "JPEG Image")
},
{
"extension": "png",
"description": i18n_catalog.i18nc("@item:inlistbox", "PNG Image")
},
{
"extension": "bmp",
"description": i18n_catalog.i18nc("@item:inlistbox", "BMP Image")
},
{
"extension": "gif",
"description": i18n_catalog.i18nc("@item:inlistbox", "GIF Image")
}
]
}
def register(app):
return { "mesh_reader": ImageReader.ImageReader() }

View file

@ -0,0 +1,280 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from UM.View.View import View
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Resources import Resources
from UM.Event import Event, KeyEvent
from UM.Signal import Signal
from UM.Scene.Selection import Selection
from UM.Math.Color import Color
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Job import Job
from UM.Preferences import Preferences
from UM.Logger import Logger
from UM.View.RenderBatch import RenderBatch
from UM.View.GL.OpenGL import OpenGL
from cura.ConvexHullNode import ConvexHullNode
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtWidgets import QApplication
from . import LayerViewProxy
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
import numpy
## View used to display g-code paths.
class LayerView(View):
def __init__(self):
super().__init__()
self._shader = None
self._selection_shader = None
self._num_layers = 0
self._layer_percentage = 0 # what percentage of layers need to be shown (Slider gives value between 0 - 100)
self._proxy = LayerViewProxy.LayerViewProxy()
self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged)
self._max_layers = 0
self._current_layer_num = 0
self._current_layer_mesh = None
self._current_layer_jumps = None
self._top_layers_job = None
self._activity = False
self._old_max_layers = 0
Preferences.getInstance().addPreference("view/top_layer_count", 5)
Preferences.getInstance().addPreference("view/only_show_top_layers", False)
Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count"))
self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers"))
self._busy = False
def getActivity(self):
return self._activity
def getCurrentLayer(self):
return self._current_layer_num
def _onSceneChanged(self, node):
self.calculateMaxLayers()
def getMaxLayers(self):
return self._max_layers
busyChanged = Signal()
def isBusy(self):
return self._busy
def setBusy(self, busy):
if busy != self._busy:
self._busy = busy
self.busyChanged.emit()
def resetLayerData(self):
self._current_layer_mesh = None
self._current_layer_jumps = None
def beginRendering(self):
scene = self.getController().getScene()
renderer = self.getRenderer()
if not self._selection_shader:
self._selection_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader"))
self._selection_shader.setUniformValue("u_color", Color(32, 32, 32, 128))
for node in DepthFirstIterator(scene.getRoot()):
# We do not want to render ConvexHullNode as it conflicts with the bottom layers.
# However, it is somewhat relevant when the node is selected, so do render it then.
if type(node) is ConvexHullNode and not Selection.isSelected(node.getWatchedNode()):
continue
if not node.render(renderer):
if node.getMeshData() and node.isVisible():
if Selection.isSelected(node):
renderer.queueNode(node, transparent = True, shader = self._selection_shader)
layer_data = node.callDecoration("getLayerData")
if not layer_data:
continue
# Render all layers below a certain number as line mesh instead of vertices.
if self._current_layer_num - self._solid_layers > -1 and not self._only_show_top_layers:
start = 0
end = 0
element_counts = layer_data.getElementCounts()
for layer, counts in element_counts.items():
if layer + self._solid_layers > self._current_layer_num:
break
end += counts
# This uses glDrawRangeElements internally to only draw a certain range of lines.
renderer.queueNode(node, mesh = layer_data, mode = RenderBatch.RenderMode.Lines, range = (start, end))
if self._current_layer_mesh:
renderer.queueNode(node, mesh = self._current_layer_mesh)
if self._current_layer_jumps:
renderer.queueNode(node, mesh = self._current_layer_jumps)
def setLayer(self, value):
if self._current_layer_num != value:
self._current_layer_num = value
if self._current_layer_num < 0:
self._current_layer_num = 0
if self._current_layer_num > self._max_layers:
self._current_layer_num = self._max_layers
self._startUpdateTopLayers()
self.currentLayerNumChanged.emit()
def calculateMaxLayers(self):
scene = self.getController().getScene()
self._activity = True
self._old_max_layers = self._max_layers
## Recalculate num max layers
new_max_layers = 0
for node in DepthFirstIterator(scene.getRoot()):
layer_data = node.callDecoration("getLayerData")
if not layer_data:
continue
if new_max_layers < len(layer_data.getLayers()):
new_max_layers = len(layer_data.getLayers()) - 1
if new_max_layers > 0 and new_max_layers != self._old_max_layers:
self._max_layers = new_max_layers
# The qt slider has a bit of weird behavior that if the maxvalue needs to be changed first
# if it's the largest value. If we don't do this, we can have a slider block outside of the
# slider.
if new_max_layers > self._current_layer_num:
self.maxLayersChanged.emit()
self.setLayer(int(self._max_layers))
else:
self.setLayer(int(self._max_layers))
self.maxLayersChanged.emit()
self._startUpdateTopLayers()
maxLayersChanged = Signal()
currentLayerNumChanged = Signal()
## Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created
# as this caused some issues.
def getProxy(self, engine, script_engine):
return self._proxy
def endRendering(self):
pass
def event(self, event):
modifiers = QApplication.keyboardModifiers()
ctrl_is_active = modifiers == Qt.ControlModifier
if event.type == Event.KeyPressEvent and ctrl_is_active:
if event.key == KeyEvent.UpKey:
self.setLayer(self._current_layer_num + 1)
return True
if event.key == KeyEvent.DownKey:
self.setLayer(self._current_layer_num - 1)
return True
def _startUpdateTopLayers(self):
if self._top_layers_job:
self._top_layers_job.finished.disconnect(self._updateCurrentLayerMesh)
self._top_layers_job.cancel()
self.setBusy(True)
self._top_layers_job = _CreateTopLayersJob(self._controller.getScene(), self._current_layer_num, self._solid_layers)
self._top_layers_job.finished.connect(self._updateCurrentLayerMesh)
self._top_layers_job.start()
def _updateCurrentLayerMesh(self, job):
self.setBusy(False)
if not job.getResult():
return
self.resetLayerData() # Reset the layer data only when job is done. Doing it now prevents "blinking" data.
self._current_layer_mesh = job.getResult().get("layers")
self._current_layer_jumps = job.getResult().get("jumps")
self._controller.getScene().sceneChanged.emit(self._controller.getScene().getRoot())
self._top_layers_job = None
def _onPreferencesChanged(self, preference):
if preference != "view/top_layer_count" and preference != "view/only_show_top_layers":
return
self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count"))
self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers"))
self._startUpdateTopLayers()
class _CreateTopLayersJob(Job):
def __init__(self, scene, layer_number, solid_layers):
super().__init__()
self._scene = scene
self._layer_number = layer_number
self._solid_layers = solid_layers
self._cancel = False
def run(self):
layer_data = None
for node in DepthFirstIterator(self._scene.getRoot()):
layer_data = node.callDecoration("getLayerData")
if layer_data:
break
if self._cancel or not layer_data:
return
layer_mesh = MeshBuilder()
for i in range(self._solid_layers):
layer_number = self._layer_number - i
if layer_number < 0:
continue
try:
layer = layer_data.getLayer(layer_number).createMesh()
except Exception:
Logger.logException("w", "An exception occurred while creating layer mesh.")
return
if not layer or layer.getVertices() is None:
continue
layer_mesh.addIndices(layer_mesh.getVertexCount() + layer.getIndices())
layer_mesh.addVertices(layer.getVertices())
# Scale layer color by a brightness factor based on the current layer number
# This will result in a range of 0.5 - 1.0 to multiply colors by.
brightness = numpy.ones((1, 4), dtype=numpy.float32) * (2.0 - (i / self._solid_layers)) / 2.0
brightness[0, 3] = 1.0
layer_mesh.addColors(layer.getColors() * brightness)
if self._cancel:
return
Job.yieldThread()
if self._cancel:
return
Job.yieldThread()
jump_mesh = layer_data.getLayer(self._layer_number).createJumps()
if not jump_mesh or jump_mesh.getVertices() is None:
jump_mesh = None
self.setResult({"layers": layer_mesh.build(), "jumps": jump_mesh})
def cancel(self):
self._cancel = True
super().cancel()

View file

@ -0,0 +1,116 @@
// Copyright (c) 2015 Ultimaker B.V.
// Cura is released under the terms of the AGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Layouts 1.1
import QtQuick.Controls.Styles 1.1
import UM 1.0 as UM
Item
{
width: UM.Theme.getSize("button").width
height: UM.Theme.getSize("slider_layerview_size").height
Slider
{
id: slider
width: UM.Theme.getSize("slider_layerview_size").width
height: UM.Theme.getSize("slider_layerview_size").height
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("slider_layerview_margin").width/2
orientation: Qt.Vertical
minimumValue: 0;
maximumValue: UM.LayerView.numLayers;
stepSize: 1
property real pixelsPerStep: ((height - UM.Theme.getSize("slider_handle").height) / (maximumValue - minimumValue)) * stepSize;
value: UM.LayerView.currentLayer
onValueChanged: UM.LayerView.setCurrentLayer(value)
style: UM.Theme.styles.slider;
Rectangle
{
x: parent.width + UM.Theme.getSize("slider_layerview_background").width / 2;
y: parent.height - (parent.value * parent.pixelsPerStep) - UM.Theme.getSize("slider_handle").height * 1.25;
height: UM.Theme.getSize("slider_handle").height + UM.Theme.getSize("default_margin").height
width: valueLabel.width + UM.Theme.getSize("default_margin").width
Behavior on height { NumberAnimation { duration: 50; } }
border.width: UM.Theme.getSize("default_lining").width;
border.color: UM.Theme.getColor("slider_groove_border");
visible: UM.LayerView.getLayerActivity && Printer.getPlatformActivity ? true : false
TextField
{
id: valueLabel
property string maxValue: slider.maximumValue + 1
text: slider.value + 1
horizontalAlignment: TextInput.AlignRight;
onEditingFinished:
{
// Ensure that the cursor is at the first position. On some systems the text isn't fully visible
// Seems to have to do something with different dpi densities that QML doesn't quite handle.
// Another option would be to increase the size even further, but that gives pretty ugly results.
cursorPosition = 0;
if(valueLabel.text != '')
{
slider.value = valueLabel.text - 1;
}
}
validator: IntValidator { bottom: 1; top: slider.maximumValue + 1; }
anchors.left: parent.left;
anchors.leftMargin: UM.Theme.getSize("default_margin").width / 2;
anchors.verticalCenter: parent.verticalCenter;
width: Math.max(UM.Theme.getSize("line").width * maxValue.length + 2, 20);
style: TextFieldStyle
{
textColor: UM.Theme.getColor("setting_control_text");
font: UM.Theme.getFont("default");
background: Item { }
}
}
BusyIndicator
{
id: busyIndicator;
anchors.left: parent.right;
anchors.leftMargin: UM.Theme.getSize("default_margin").width / 2;
anchors.verticalCenter: parent.verticalCenter;
width: UM.Theme.getSize("slider_handle").height;
height: width;
running: UM.LayerView.busy;
visible: UM.LayerView.busy;
}
}
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
z: slider.z - 1
width: UM.Theme.getSize("slider_layerview_background").width
height: slider.height + UM.Theme.getSize("default_margin").height * 2
color: UM.Theme.getColor("tool_panel_background");
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
MouseArea {
id: sliderMouseArea
property double manualStepSize: slider.maximumValue / 11
anchors.fill: parent
onWheel: {
slider.value = wheel.angleDelta.y < 0 ? slider.value - sliderMouseArea.manualStepSize : slider.value + sliderMouseArea.manualStepSize
}
}
}
}

View file

@ -0,0 +1,70 @@
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
from UM.Application import Application
import LayerView
class LayerViewProxy(QObject):
def __init__(self, parent = None):
super().__init__(parent)
self._current_layer = 0
self._controller = Application.getInstance().getController()
self._controller.activeViewChanged.connect(self._onActiveViewChanged)
self._onActiveViewChanged()
currentLayerChanged = pyqtSignal()
maxLayersChanged = pyqtSignal()
activityChanged = pyqtSignal()
@pyqtProperty(bool, notify = activityChanged)
def getLayerActivity(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
return active_view.getActivity()
@pyqtProperty(int, notify = maxLayersChanged)
def numLayers(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
return active_view.getMaxLayers()
#return 100
@pyqtProperty(int, notify = currentLayerChanged)
def currentLayer(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
return active_view.getCurrentLayer()
busyChanged = pyqtSignal()
@pyqtProperty(bool, notify = busyChanged)
def busy(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
return active_view.isBusy()
return False
@pyqtSlot(int)
def setCurrentLayer(self, layer_num):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
active_view.setLayer(layer_num)
def _layerActivityChanged(self):
self.activityChanged.emit()
def _onLayerChanged(self):
self.currentLayerChanged.emit()
self._layerActivityChanged()
def _onMaxLayersChanged(self):
self.maxLayersChanged.emit()
def _onBusyChanged(self):
self.busyChanged.emit()
def _onActiveViewChanged(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
active_view.currentLayerNumChanged.connect(self._onLayerChanged)
active_view.maxLayersChanged.connect(self._onMaxLayersChanged)
active_view.busyChanged.connect(self._onBusyChanged)

View file

@ -0,0 +1,32 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from . import LayerView, LayerViewProxy
from PyQt5.QtQml import qmlRegisterType, qmlRegisterSingletonType
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {
"plugin": {
"name": catalog.i18nc("@label", "Layer View"),
"author": "Ultimaker",
"version": "1.0",
"description": catalog.i18nc("@info:whatsthis", "Provides the Layer view."),
"api": 3
},
"view": {
"name": catalog.i18nc("@item:inlistbox", "Layers"),
"view_panel": "LayerView.qml",
"weight": 2
}
}
def createLayerViewProxy(engine, script_engine):
return LayerViewProxy.LayerViewProxy()
def register(app):
layer_view = LayerView.LayerView()
qmlRegisterSingletonType(LayerViewProxy.LayerViewProxy, "UM", 1, 0, "LayerView", layer_view.getProxy)
return { "view": LayerView.LayerView() }

View file

@ -0,0 +1,157 @@
{
"source_version": "15.04",
"target_version": 1,
"translation": {
"machine_nozzle_size": "nozzle_size",
"line_width": "wall_thickness if (spiralize == \"True\" or simple_mode == \"True\") else (nozzle_size if (float(wall_thickness) < 0.01) else (wall_thickness if (float(wall_thickness) < float(nozzle_size)) else (nozzle_size if ((int(float(wall_thickness) / (float(nozzle_size) - 0.0001))) == 0) else ((float(wall_thickness) / ((int(float(wall_thickness) / (float(nozzle_size) - 0.0001))) + 1)) if ((float(wall_thickness) / (int(float(wall_thickness) / (float(nozzle_size) - 0.0001)))) > float(nozzle_size) * 1.5) else ((float(wall_thickness) / (int(float(wall_thickness) / (float(nozzle_size) - 0.0001)))))))))",
"layer_height": "layer_height",
"layer_height_0": "bottom_thickness",
"wall_thickness": "wall_thickness",
"top_thickness": "0 if (solid_top == \"False\") else solid_layer_thickness",
"bottom_thickness": "0 if (solid_bottom == \"False\") else solid_layer_thickness",
"infill_sparse_density": "fill_density",
"infill_overlap": "fill_overlap",
"infill_before_walls": "False if (perimeter_before_infill == \"True\") else True",
"material_print_temperature": "print_temperature",
"material_bed_temperature": "print_bed_temperature",
"material_diameter": "filament_diameter",
"material_flow": "filament_flow",
"retraction_enable": "retraction_enable",
"retraction_amount": "retraction_amount",
"retraction_speed": "retraction_speed",
"retraction_min_travel": "retraction_min_travel",
"retraction_hop": "retraction_hop",
"speed_print": "print_speed",
"speed_infill": "infill_speed if (float(infill_speed) != 0) else print_speed",
"speed_wall_0": "inset0_speed if (float(inset0_speed) != 0) else print_speed",
"speed_wall_x": "insetx_speed if (float(insetx_speed) != 0) else print_speed",
"speed_topbottom": "solidarea_speed if (float(solidarea_speed) != 0) else print_speed",
"speed_travel": "travel_speed if (float(travel_speed) != 0) else travel_speed",
"speed_layer_0": "bottom_layer_speed",
"retraction_combing": "True if (retraction_combing == \"All\" or retraction_combing == \"No Skin\") else False",
"cool_fan_enabled": "fan_enabled",
"cool_fan_speed_min": "fan_speed",
"cool_fan_speed_max": "fan_speed_max",
"cool_fan_full_at_height": "fan_full_height",
"cool_min_layer_time": "cool_min_layer_time",
"cool_min_speed": "cool_min_feedrate",
"cool_lift_head": "cool_head_lift",
"support_enable": "False if (support == \"None\") else True",
"support_type": "\"buildplate\" if (support == \"Touching buildplate\") else \"everywhere\"",
"support_angle": "support_angle",
"support_xy_distance": "support_xy_distance",
"support_z_distance": "support_z_distance",
"support_pattern": "support_type.lower()",
"support_infill_rate": "support_fill_rate",
"adhesion_type": "\"skirt\" if (platform_adhesion == \"None\") else platform_adhesion.lower()",
"skirt_line_count": "skirt_line_count",
"skirt_gap": "skirt_gap",
"skirt_brim_minimal_length": "skirt_minimal_length",
"brim_line_count": "brim_line_count",
"raft_margin": "raft_margin",
"raft_airgap": "float(raft_airgap_all) + float(raft_airgap)",
"layer_0_z_overlap": "raft_airgap",
"raft_surface_layers": "raft_surface_layers",
"raft_surface_thickness": "raft_surface_thickness",
"raft_surface_line_width": "raft_surface_linewidth",
"raft_surface_line_spacing": "raft_line_spacing",
"raft_interface_thickness": "raft_interface_thickness",
"raft_interface_line_width": "raft_interface_linewidth",
"raft_interface_line_spacing": "raft_line_spacing",
"raft_base_thickness": "raft_base_thickness",
"raft_base_line_width": "raft_base_linewidth",
"raft_base_line_spacing": "raft_line_spacing",
"meshfix_union_all": "fix_horrible_union_all_type_a",
"meshfix_union_all_remove_holes": "fix_horrible_union_all_type_b",
"meshfix_extensive_stitching": "fix_horrible_extensive_stitching",
"meshfix_keep_open_polygons": "fix_horrible_use_open_bits",
"magic_mesh_surface_mode": "simple_mode",
"magic_spiralize": "spiralize",
"prime_tower_enable": "wipe_tower",
"prime_tower_size": "math.sqrt(float(wipe_tower_volume) / float(layer_height))",
"ooze_shield_enabled": "ooze_shield"
},
"defaults": {
"bottom_layer_speed": "20",
"bottom_thickness": "0.3",
"brim_line_count": "20",
"cool_head_lift": "False",
"cool_min_feedrate": "10",
"cool_min_layer_time": "5",
"fan_enabled": "True",
"fan_full_height": "0.5",
"fan_speed": "100",
"fan_speed_max": "100",
"filament_diameter": "2.85",
"filament_diameter2": "0",
"filament_diameter3": "0",
"filament_diameter4": "0",
"filament_diameter5": "0",
"filament_flow": "100.0",
"fill_density": "20",
"fill_overlap": "15",
"fix_horrible_extensive_stitching": "False",
"fix_horrible_union_all_type_a": "True",
"fix_horrible_union_all_type_b": "False",
"fix_horrible_use_open_bits": "False",
"infill_speed": "0.0",
"inset0_speed": "0.0",
"insetx_speed": "0.0",
"layer_height": "0.1",
"layer0_width_factor": "100",
"nozzle_size": "0.4",
"object_sink": "0.0",
"ooze_shield": "False",
"overlap_dual": "0.15",
"perimeter_before_infill": "False",
"platform_adhesion": "None",
"print_bed_temperature": "70",
"print_speed": "50",
"print_temperature": "210",
"print_temperature2": "0",
"print_temperature3": "0",
"print_temperature4": "0",
"print_temperature5": "0",
"raft_airgap": "0.22",
"raft_airgap_all": "0.0",
"raft_base_linewidth": "1.0",
"raft_base_thickness": "0.3",
"raft_interface_linewidth": "0.4",
"raft_interface_thickness": "0.27",
"raft_line_spacing": "3.0",
"raft_margin": "5.0",
"raft_surface_layers": "2",
"raft_surface_linewidth": "0.4",
"raft_surface_thickness": "0.27",
"retraction_amount": "4.5",
"retraction_combing": "All",
"retraction_dual_amount": "16.5",
"retraction_enable": "True",
"retraction_hop": "0.0",
"retraction_min_travel": "1.5",
"retraction_minimal_extrusion": "0.02",
"retraction_speed": "40.0",
"simple_mode": "False",
"skirt_gap": "3.0",
"skirt_line_count": "1",
"skirt_minimal_length": "150.0",
"solid_bottom": "True",
"solid_layer_thickness": "0.6",
"solid_top": "True",
"solidarea_speed": "0.0",
"spiralize": "False",
"support": "None",
"support_angle": "60",
"support_dual_extrusion": "Both",
"support_fill_rate": "15",
"support_type": "Lines",
"support_xy_distance": "0.7",
"support_z_distance": "0.15",
"travel_speed": "150.0",
"wall_thickness": "0.8",
"wipe_tower": "False",
"wipe_tower_volume": "15"
}
}

View file

@ -0,0 +1,130 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
import configparser #For reading the legacy profile INI files.
import json #For reading the Dictionary of Doom.
import math #For mathematical operations included in the Dictionary of Doom.
import os.path #For concatenating the path to the plugin and the relative path to the Dictionary of Doom.
from UM.Application import Application #To get the machine manager to create the new profile in.
from UM.Logger import Logger #Logging errors.
from UM.PluginRegistry import PluginRegistry #For getting the path to this plugin's directory.
from UM.Settings.DefinitionContainer import DefinitionContainer #For getting the current machine's defaults.
from UM.Settings.InstanceContainer import InstanceContainer #The new profile to make.
from cura.ProfileReader import ProfileReader #The plug-in type to implement.
## A plugin that reads profile data from legacy Cura versions.
#
# It reads a profile from an .ini file, and performs some translations on it.
# Not all translations are correct, mind you, but it is a best effort.
class LegacyProfileReader(ProfileReader):
## Initialises the legacy profile reader.
#
# This does nothing since the only other function is basically stateless.
def __init__(self):
super().__init__()
## Prepares the default values of all legacy settings.
#
# These are loaded from the Dictionary of Doom.
#
# \param json The JSON file to load the default setting values from. This
# should not be a URL but a pre-loaded JSON handle.
# \return A dictionary of the default values of the legacy Cura version.
def prepareDefaults(self, json):
defaults = {}
for key in json["defaults"]: #We have to copy over all defaults from the JSON handle to a normal dict.
defaults[key] = json["defaults"][key]
return defaults
## Prepares the local variables that can be used in evaluation of computing
# new setting values from the old ones.
#
# This fills a dictionary with all settings from the legacy Cura version
# and their values, so that they can be used in evaluating the new setting
# values as Python code.
#
# \param config_parser The ConfigParser that finds the settings in the
# legacy profile.
# \param config_section The section in the profile where the settings
# should be found.
# \param defaults The default values for all settings in the legacy Cura.
# \return A set of local variables, one for each setting in the legacy
# profile.
def prepareLocals(self, config_parser, config_section, defaults):
copied_locals = defaults.copy() #Don't edit the original!
for option in config_parser.options(config_section):
copied_locals[option] = config_parser.get(config_section, option)
return copied_locals
## Reads a legacy Cura profile from a file and returns it.
#
# \param file_name The file to read the legacy Cura profile from.
# \return The legacy Cura profile that was in the file, if any. If the
# file could not be read or didn't contain a valid profile, \code None
# \endcode is returned.
def read(self, file_name):
if file_name.split(".")[-1] != "ini":
return None
Logger.log("i", "Importing legacy profile from file " + file_name + ".")
profile = InstanceContainer("Imported Legacy Profile") #Create an empty profile.
parser = configparser.ConfigParser(interpolation = None)
try:
with open(file_name) as f:
parser.readfp(f) #Parse the INI file.
except Exception as e:
Logger.log("e", "Unable to open legacy profile %s: %s", file_name, str(e))
return None
#Legacy Cura saved the profile under the section "profile_N" where N is the ID of a machine, except when you export in which case it saves it in the section "profile".
#Since importing multiple machine profiles is out of scope, just import the first section we find.
section = ""
for found_section in parser.sections():
if found_section.startswith("profile"):
section = found_section
break
if not section: #No section starting with "profile" was found. Probably not a proper INI file.
return None
try:
with open(os.path.join(PluginRegistry.getInstance().getPluginPath("LegacyProfileReader"), "DictionaryOfDoom.json"), "r", -1, "utf-8") as f:
dict_of_doom = json.load(f) #Parse the Dictionary of Doom.
except IOError as e:
Logger.log("e", "Could not open DictionaryOfDoom.json for reading: %s", str(e))
return None
except Exception as e:
Logger.log("e", "Could not parse DictionaryOfDoom.json: %s", str(e))
return None
defaults = self.prepareDefaults(dict_of_doom)
legacy_settings = self.prepareLocals(parser, section, defaults) #Gets the settings from the legacy profile.
#Check the target version in the Dictionary of Doom with this application version.
if "target_version" not in dict_of_doom:
Logger.log("e", "Dictionary of Doom has no target version. Is it the correct JSON file?")
return None
if InstanceContainer.Version != dict_of_doom["target_version"]:
Logger.log("e", "Dictionary of Doom of legacy profile reader (version %s) is not in sync with the current instance container version (version %s)!", dict_of_doom["target_version"], str(InstanceContainer.Version))
return None
if "translation" not in dict_of_doom:
Logger.log("e", "Dictionary of Doom has no translation. Is it the correct JSON file?")
return None
current_printer = Application.getInstance().getGlobalContainerStack().findContainer({ }, DefinitionContainer)
for new_setting in dict_of_doom["translation"]: #Evaluate all new settings that would get a value from the translations.
old_setting_expression = dict_of_doom["translation"][new_setting]
compiled = compile(old_setting_expression, new_setting, "eval")
try:
new_value = eval(compiled, {"math": math}, legacy_settings) #Pass the legacy settings as local variables to allow access to in the evaluation.
value_using_defaults = eval(compiled, {"math": math}, defaults) #Evaluate again using only the default values to try to see if they are default.
except Exception: #Probably some setting name that was missing or something else that went wrong in the ini file.
Logger.log("w", "Setting " + new_setting + " could not be set because the evaluation failed. Something is probably missing from the imported legacy profile.")
continue
if new_value != value_using_defaults and current_printer.findDefinitions(key = new_setting).default_value != new_value: #Not equal to the default in the new Cura OR the default in the legacy Cura.
profile.setSettingValue(new_setting, new_value) #Store the setting in the profile!
if len(profile.getChangedSettings()) == 0:
Logger.log("i", "A legacy profile was imported but everything evaluates to the defaults, creating an empty profile.")
profile.setDirty(True)
return profile

View file

@ -0,0 +1,27 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from . import LegacyProfileReader
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {
"plugin": {
"name": catalog.i18nc("@label", "Legacy Cura Profile Reader"),
"author": "Ultimaker",
"version": "1.0",
"description": catalog.i18nc("@info:whatsthis", "Provides support for importing profiles from legacy Cura versions."),
"api": 3
},
"profile_reader": [
{
"extension": "ini",
"description": catalog.i18nc("@item:inlistbox", "Cura 15.04 profiles")
}
]
}
def register(app):
return { "profile_reader": LegacyProfileReader.LegacyProfileReader() }

View file

@ -0,0 +1,62 @@
// Copyright (c) 2015 Ultimaker B.V.
// Uranium is released under the terms of the AGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
import QtQuick.Layouts 1.1
import UM 1.1 as UM
import ".."
Button {
id: base;
style: ButtonStyle {
background: Item { }
label: Row
{
spacing: UM.Theme.getSize("default_lining").width
UM.RecolorImage
{
anchors.verticalCenter: parent.verticalCenter
height: label.height / 2
width: height
source: control.checked ? UM.Theme.getIcon("arrow_bottom") : UM.Theme.getIcon("arrow_right");
color: control.hovered ? palette.highlight : palette.buttonText
}
UM.RecolorImage
{
anchors.verticalCenter: parent.verticalCenter
height: label.height
width: height
source: control.iconSource
color: control.hovered ? palette.highlight : palette.buttonText
}
Label
{
id: label
anchors.verticalCenter: parent.verticalCenter
text: control.text
color: control.hovered ? palette.highlight : palette.buttonText
font.bold: true
}
SystemPalette { id: palette }
}
}
signal showTooltip(string text);
signal hideTooltip();
signal contextMenuRequested()
text: definition.label
iconSource: UM.Theme.getIcon(definition.icon)
checkable: true
checked: definition.expanded
onClicked: definition.expanded ? settingDefinitionsModel.collapse(definition.key) : settingDefinitionsModel.expandAll(definition.key)
}

View file

@ -0,0 +1,34 @@
// Copyright (c) 2015 Ultimaker B.V.
// Uranium is released under the terms of the AGPLv3 or higher.
import QtQuick 2.1
import QtQuick.Layouts 1.1
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
import UM 1.2 as UM
UM.TooltipArea
{
x: model.depth * UM.Theme.getSize("default_margin").width;
text: model.description;
width: childrenRect.width;
height: childrenRect.height;
CheckBox
{
id: check
text: definition.label
checked: addedSettingsModel.getVisible(model.key)
onClicked:
{
addedSettingsModel.setVisible(model.key, checked);
UM.ActiveTool.forceUpdate();
}
}
}

View file

@ -0,0 +1,96 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
from UM.Application import Application
from UM.Settings.SettingInstance import SettingInstance
from UM.Logger import Logger
import UM.Settings.Models
from cura.Settings.ExtruderManager import ExtruderManager #To get global-inherits-stack setting values from different extruders.
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
## The per object setting visibility handler ensures that only setting
# definitions that have a matching instance Container are returned as visible.
class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHandler):
def __init__(self, parent = None, *args, **kwargs):
super().__init__(parent = parent, *args, **kwargs)
self._selected_object_id = None
self._node = None
self._stack = None
def setSelectedObjectId(self, id):
if id != self._selected_object_id:
self._selected_object_id = id
self._node = Application.getInstance().getController().getScene().findObject(self._selected_object_id)
if self._node:
self._stack = self._node.callDecoration("getStack")
self.visibilityChanged.emit()
@pyqtProperty("quint64", fset = setSelectedObjectId)
def selectedObjectId(self):
return self._selected_object_id
def setVisible(self, visible):
if not self._node:
return
if not self._stack:
self._node.addDecorator(SettingOverrideDecorator())
self._stack = self._node.callDecoration("getStack")
settings = self._stack.getTop()
all_instances = settings.findInstances()
visibility_changed = False # Flag to check if at the end the signal needs to be emitted
# Remove all instances that are not in visibility list
for instance in all_instances:
if instance.definition.key not in visible:
settings.removeInstance(instance.definition.key)
visibility_changed = True
# Add all instances that are not added, but are in visibility list
for item in visible:
if not settings.getInstance(item):
definition = self._stack.getSettingDefinition(item)
if definition:
new_instance = SettingInstance(definition, settings)
stack_nr = -1
if definition.global_inherits_stack and self._stack.getProperty("machine_extruder_count", "value") > 1:
#Obtain the value from the correct container stack. Only once, upon adding the setting.
stack_nr = str(int(round(float(self._stack.getProperty(item, "global_inherits_stack"))))) #Stack to get the setting from. Round it and remove the fractional part.
if stack_nr not in ExtruderManager.getInstance().extruderIds and self._stack.getProperty("extruder_nr", "value"): #Property not defined, but we have an extruder number.
stack_nr = str(int(round(float(self._stack.getProperty("extruder_nr", "value")))))
if stack_nr in ExtruderManager.getInstance().extruderIds: #We have either a global_inherits_stack or an extruder_nr.
stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = ExtruderManager.getInstance().extruderIds[stack_nr])[0]
else:
stack = UM.Application.getInstance().getGlobalContainerStack()
new_instance.setProperty("value", stack.getProperty(item, "value"))
new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance)
visibility_changed = True
else:
Logger.log("w", "Unable to add instance (%s) to per-object visibility because we couldn't find the matching definition", item)
if visibility_changed:
self.visibilityChanged.emit()
def getVisible(self):
visible_settings = set()
if not self._node:
return visible_settings
if not self._stack:
return visible_settings
settings = self._stack.getTop()
if not settings:
return visible_settings
visible_settings = set(map(lambda i: i.definition.key, settings.findInstances()))
return visible_settings

View file

@ -0,0 +1,469 @@
// Copyright (c) 2016 Ultimaker B.V.
// Uranium is released under the terms of the AGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Controls.Styles 1.2
import QtQuick.Window 2.2
import UM 1.2 as UM
import Cura 1.0 as Cura
import ".."
Item {
id: base;
UM.I18nCatalog { id: catalog; name: "cura"; }
width: childrenRect.width;
height: childrenRect.height;
Column
{
id: items
anchors.top: parent.top;
anchors.left: parent.left;
spacing: UM.Theme.getSize("default_margin").height
Row
{
spacing: UM.Theme.getSize("default_margin").width
Label
{
text: catalog.i18nc("@label", "Print model with")
anchors.verticalCenter: extruderSelector.verticalCenter
color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default")
visible: extruderSelector.visible
}
ComboBox
{
id: extruderSelector
model: Cura.ExtrudersModel
{
id: extruders_model
onRowsInserted: extruderSelector.visible = extruders_model.rowCount() > 1
onModelReset: extruderSelector.visible = extruders_model.rowCount() > 1
onModelChanged: extruderSelector.color = extruders_model.getItem(extruderSelector.currentIndex).color
}
property string color: extruders_model.getItem(extruderSelector.currentIndex).color
visible: extruders_model.rowCount() > 1
textRole: "name"
width: UM.Theme.getSize("setting_control").width
height: UM.Theme.getSize("section").height
MouseArea
{
anchors.fill: parent
acceptedButtons: Qt.NoButton
onWheel: wheel.accepted = true;
}
style: ComboBoxStyle
{
background: Rectangle
{
color:
{
if(extruderSelector.hovered || base.activeFocus)
{
return UM.Theme.getColor("setting_control_highlight");
}
else
{
return UM.Theme.getColor("setting_control");
}
}
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("setting_control_border")
}
label: Item
{
Rectangle
{
id: swatch
height: UM.Theme.getSize("setting_control").height / 2
width: height
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_lining").width
anchors.verticalCenter: parent.verticalCenter
color: extruderSelector.color
border.width: UM.Theme.getSize("default_lining").width
border.color: !enabled ? UM.Theme.getColor("setting_control_disabled_border") : UM.Theme.getColor("setting_control_border")
}
Label
{
anchors.left: swatch.right
anchors.leftMargin: UM.Theme.getSize("default_lining").width
anchors.right: downArrow.left
anchors.rightMargin: UM.Theme.getSize("default_lining").width
anchors.verticalCenter: parent.verticalCenter
text: extruderSelector.currentText
font: UM.Theme.getFont("default")
color: !enabled ? UM.Theme.getColor("setting_control_disabled_text") : UM.Theme.getColor("setting_control_text")
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
UM.RecolorImage
{
id: downArrow
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_lining").width * 2
anchors.verticalCenter: parent.verticalCenter
source: UM.Theme.getIcon("arrow_bottom")
width: UM.Theme.getSize("standard_arrow").width
height: UM.Theme.getSize("standard_arrow").height
sourceSize.width: width + 5
sourceSize.height: width + 5
color: UM.Theme.getColor("setting_control_text")
}
}
}
onActivated:
{
UM.ActiveTool.setProperty("SelectedActiveExtruder", extruders_model.getItem(index).id);
extruderSelector.color = extruders_model.getItem(index).color;
}
onModelChanged: updateCurrentIndex();
function updateCurrentIndex()
{
for(var i = 0; i < extruders_model.rowCount(); ++i)
{
if(extruders_model.getItem(i).id == UM.ActiveTool.properties.getValue("SelectedActiveExtruder"))
{
extruderSelector.currentIndex = i;
extruderSelector.color = extruders_model.getItem(i).color;
return;
}
}
extruderSelector.currentIndex = -1;
}
}
}
Column
{
spacing: UM.Theme.getSize("default_lining").height
Repeater
{
id: contents
height: childrenRect.height;
model: UM.SettingDefinitionsModel
{
id: addedSettingsModel;
containerId: Cura.MachineManager.activeDefinitionId
expanded: [ "*" ]
visibilityHandler: Cura.PerObjectSettingVisibilityHandler
{
selectedObjectId: UM.ActiveTool.properties.getValue("SelectedObjectId")
}
}
delegate: Row
{
Loader
{
id: settingLoader
width: UM.Theme.getSize("setting").width
height: UM.Theme.getSize("section").height
property var definition: model
property var settingDefinitionsModel: addedSettingsModel
property var propertyProvider: provider
//Qt5.4.2 and earlier has a bug where this causes a crash: https://bugreports.qt.io/browse/QTBUG-35989
//In addition, while it works for 5.5 and higher, the ordering of the actual combo box drop down changes,
//causing nasty issues when selecting different options. So disable asynchronous loading of enum type completely.
asynchronous: model.type != "enum" && model.type != "extruder"
onLoaded: {
settingLoader.item.showRevertButton = false
settingLoader.item.showInheritButton = false
settingLoader.item.showLinkedSettingIcon = false
settingLoader.item.doDepthIndentation = false
settingLoader.item.doQualityUserSettingEmphasis = false
}
sourceComponent:
{
switch(model.type)
{
case "int":
return settingTextField
case "float":
return settingTextField
case "enum":
return settingComboBox
case "extruder":
return settingExtruder
case "bool":
return settingCheckBox
case "str":
return settingTextField
case "category":
return settingCategory
default:
return settingUnknown
}
}
}
Button
{
width: UM.Theme.getSize("setting").height / 2;
height: UM.Theme.getSize("setting").height;
onClicked: addedSettingsModel.setVisible(model.key, false);
style: ButtonStyle
{
background: Item
{
UM.RecolorImage
{
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: parent.height / 2
sourceSize.width: width
sourceSize.height: width
color: control.hovered ? UM.Theme.getColor("setting_control_button_hover") : UM.Theme.getColor("setting_control_button")
source: UM.Theme.getIcon("minus")
}
}
}
}
UM.SettingPropertyProvider
{
id: provider
containerStackId: UM.ActiveTool.properties.getValue("ContainerID")
key: model.key
watchedProperties: [ "value", "enabled", "validationState" ]
storeIndex: 0
removeUnusedValue: false
}
}
}
}
Button
{
id: customise_settings_button;
height: UM.Theme.getSize("setting").height;
visible: parseInt(UM.Preferences.getValue("cura/active_mode")) == 1
text: catalog.i18nc("@action:button", "Select settings");
style: ButtonStyle
{
background: Rectangle
{
width: control.width;
height: control.height;
border.width: UM.Theme.getSize("default_lining").width;
border.color: control.pressed ? UM.Theme.getColor("action_button_active_border") :
control.hovered ? UM.Theme.getColor("action_button_hovered_border") : UM.Theme.getColor("action_button_border")
color: control.pressed ? UM.Theme.getColor("action_button_active") :
control.hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("action_button")
}
label: Label
{
text: control.text;
color: UM.Theme.getColor("setting_control_text");
anchors.centerIn: parent
}
}
onClicked: settingPickDialog.visible = true;
Connections
{
target: UM.Preferences;
onPreferenceChanged:
{
customise_settings_button.visible = parseInt(UM.Preferences.getValue("cura/active_mode"))
}
}
}
}
UM.Dialog {
id: settingPickDialog
title: catalog.i18nc("@title:window", "Select Settings to Customize for this model")
width: screenScaleFactor * 360;
property string labelFilter: ""
onVisibilityChanged:
{
// force updating the model to sync it with addedSettingsModel
if(visible)
{
listview.model.forceUpdate()
}
}
TextField {
id: filter
anchors {
top: parent.top
left: parent.left
right: toggleShowAll.left
rightMargin: UM.Theme.getSize("default_margin").width
}
placeholderText: catalog.i18nc("@label:textbox", "Filter...");
onTextChanged:
{
if(text != "")
{
listview.model.filter = {"settable_per_mesh": true, "label": "*" + text}
}
else
{
listview.model.filter = {"settable_per_mesh": true}
}
}
}
CheckBox
{
id: toggleShowAll
anchors {
top: parent.top
right: parent.right
}
text: catalog.i18nc("@label:checkbox", "Show all")
checked: listview.model.showAll
onClicked:
{
listview.model.showAll = checked;
}
}
ScrollView
{
id: scrollView
anchors
{
top: filter.bottom;
left: parent.left;
right: parent.right;
bottom: parent.bottom;
}
ListView
{
id:listview
model: UM.SettingDefinitionsModel
{
id: definitionsModel;
containerId: Cura.MachineManager.activeDefinitionId
filter:
{
"settable_per_mesh": true
}
visibilityHandler: UM.SettingPreferenceVisibilityHandler {}
expanded: [ "*" ]
exclude: [ "machine_settings" ]
}
delegate:Loader
{
id: loader
width: parent.width
height: model.type != undefined ? UM.Theme.getSize("section").height : 0;
property var definition: model
property var settingDefinitionsModel: definitionsModel
asynchronous: true
source:
{
switch(model.type)
{
case "category":
return "PerObjectCategory.qml"
default:
return "PerObjectItem.qml"
}
}
}
}
}
rightButtons: [
Button {
text: catalog.i18nc("@action:button", "Close");
onClicked: {
settingPickDialog.visible = false;
}
}
]
}
SystemPalette { id: palette; }
Component
{
id: settingTextField;
Cura.SettingTextField { }
}
Component
{
id: settingComboBox;
Cura.SettingComboBox { }
}
Component
{
id: settingExtruder;
Cura.SettingExtruder { }
}
Component
{
id: settingCheckBox;
Cura.SettingCheckBox { }
}
Component
{
id: settingCategory;
Cura.SettingCategory { }
}
Component
{
id: settingUnknown;
Cura.SettingUnknown { }
}
}

View file

@ -0,0 +1,82 @@
# Copyright (c) 2016 Ultimaker B.V.
# Uranium is released under the terms of the AGPLv3 or higher.
from UM.Tool import Tool
from UM.Scene.Selection import Selection
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Application import Application
from UM.Preferences import Preferences
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
## This tool allows the user to add & change settings per node in the scene.
# The settings per object are kept in a ContainerStack, which is linked to a node by decorator.
class PerObjectSettingsTool(Tool):
def __init__(self):
super().__init__()
self._model = None
self.setExposedProperties("SelectedObjectId", "ContainerID", "SelectedActiveExtruder")
self._advanced_mode = False
self._multi_extrusion = False
Selection.selectionChanged.connect(self.propertyChanged)
Preferences.getInstance().preferenceChanged.connect(self._onPreferenceChanged)
self._onPreferenceChanged("cura/active_mode")
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
self._onGlobalContainerChanged()
def event(self, event):
return False
def getSelectedObjectId(self):
selected_object = Selection.getSelectedObject(0)
selected_object_id = id(selected_object)
return selected_object_id
def getContainerID(self):
selected_object = Selection.getSelectedObject(0)
try:
return selected_object.callDecoration("getStack").getId()
except AttributeError:
return ""
## Gets the active extruder of the currently selected object.
#
# \return The active extruder of the currently selected object.
def getSelectedActiveExtruder(self):
selected_object = Selection.getSelectedObject(0)
return selected_object.callDecoration("getActiveExtruder")
## Changes the active extruder of the currently selected object.
#
# \param extruder_stack_id The ID of the extruder to print the currently
# selected object with.
def setSelectedActiveExtruder(self, extruder_stack_id):
selected_object = Selection.getSelectedObject(0)
stack = selected_object.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
if not stack:
selected_object.addDecorator(SettingOverrideDecorator())
selected_object.callDecoration("setActiveExtruder", extruder_stack_id)
def _onPreferenceChanged(self, preference):
if preference == "cura/active_mode":
self._advanced_mode = Preferences.getInstance().getValue(preference) == 1
self._updateEnabled()
def _onGlobalContainerChanged(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
self._multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
if not self._multi_extrusion:
# Ensure that all extruder data is reset
root_node = Application.getInstance().getController().getScene().getRoot()
for node in DepthFirstIterator(root_node):
node.callDecoration("setActiveExtruder", global_container_stack.getId())
self._updateEnabled()
def _updateEnabled(self):
Application.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, self._advanced_mode or self._multi_extrusion)

View file

@ -0,0 +1,32 @@
# Copyright (c) 2015 Ultimaker B.V.
# Uranium is released under the terms of the AGPLv3 or higher.
from . import PerObjectSettingsTool
from . import PerObjectSettingVisibilityHandler
from PyQt5.QtQml import qmlRegisterType
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
def getMetaData():
return {
"plugin": {
"name": i18n_catalog.i18nc("@label", "Per Model Settings Tool"),
"author": "Ultimaker",
"version": "1.0",
"description": i18n_catalog.i18nc("@info:whatsthis", "Provides the Per Model Settings."),
"api": 3
},
"tool": {
"name": i18n_catalog.i18nc("@label", "Per Model Settings"),
"description": i18n_catalog.i18nc("@info:tooltip", "Configure Per Model Settings"),
"icon": "setting_per_object",
"tool_panel": "PerObjectSettingsPanel.qml",
"weight": 3
},
}
def register(app):
qmlRegisterType(PerObjectSettingVisibilityHandler.PerObjectSettingVisibilityHandler, "Cura", 1, 0,
"PerObjectSettingVisibilityHandler")
return { "tool": PerObjectSettingsTool.PerObjectSettingsTool() }

View file

@ -0,0 +1,44 @@
# Copyright (c) 2015 Ultimaker B.V.
# Copyright (c) 2013 David Braam
# Uranium is released under the terms of the AGPLv3 or higher.
from . import RemovableDrivePlugin
from UM.Logger import Logger
import glob
import os
import subprocess
## Support for removable devices on Linux.
#
# TODO: This code uses the most basic interfaces for handling this.
# We should instead use UDisks2 to handle mount/unmount and hotplugging events.
#
class LinuxRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin):
def checkRemovableDrives(self):
drives = {}
for volume in glob.glob("/media/*"):
if os.path.ismount(volume):
drives[volume] = os.path.basename(volume)
elif volume == "/media/"+os.getenv("USER"):
for volume in glob.glob("/media/"+os.getenv("USER")+"/*"):
if os.path.ismount(volume):
drives[volume] = os.path.basename(volume)
for volume in glob.glob("/run/media/" + os.getenv("USER") + "/*"):
if os.path.ismount(volume):
drives[volume] = os.path.basename(volume)
return drives
def performEjectDevice(self, device):
p = subprocess.Popen(["umount", device.getId()], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output = p.communicate()
Logger.log("d", "umount returned: %s.", repr(output))
return_code = p.wait()
if return_code != 0:
return False
else:
return True

View file

@ -0,0 +1,57 @@
# Copyright (c) 2015 Ultimaker B.V.
# Copyright (c) 2013 David Braam
# Uranium is released under the terms of the AGPLv3 or higher.
from . import RemovableDrivePlugin
from UM.Logger import Logger
import subprocess
import os
import plistlib
## Support for removable devices on Mac OSX
class OSXRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin):
def checkRemovableDrives(self):
drives = {}
p = subprocess.Popen(["system_profiler", "SPUSBDataType", "-xml"], stdout = subprocess.PIPE)
plist = plistlib.loads(p.communicate()[0])
p.wait()
for entry in plist:
if "_items" in entry:
for item in entry["_items"]:
for dev in item["_items"]:
if "removable_media" in dev and dev["removable_media"] == "yes" and "volumes" in dev and len(dev["volumes"]) > 0:
for vol in dev["volumes"]:
if "mount_point" in vol:
volume = vol["mount_point"]
drives[volume] = os.path.basename(volume)
p = subprocess.Popen(["system_profiler", "SPCardReaderDataType", "-xml"], stdout=subprocess.PIPE)
plist = plistlib.loads(p.communicate()[0])
p.wait()
for entry in plist:
if "_items" in entry:
for item in entry["_items"]:
for dev in item["_items"]:
if "removable_media" in dev and dev["removable_media"] == "yes" and "volumes" in dev and len(dev["volumes"]) > 0:
for vol in dev["volumes"]:
if "mount_point" in vol:
volume = vol["mount_point"]
drives[volume] = os.path.basename(volume)
return drives
def performEjectDevice(self, device):
p = subprocess.Popen(["diskutil", "eject", device.getId()], stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
output = p.communicate()
Logger.log("d", "umount returned: %s.", repr(output))
return_code = p.wait()
if return_code != 0:
return False
else:
return True

View file

@ -0,0 +1,116 @@
import os.path
from UM.Application import Application
from UM.Logger import Logger
from UM.Message import Message
from UM.Mesh.WriteMeshJob import WriteMeshJob
from UM.Mesh.MeshWriter import MeshWriter
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.OutputDevice.OutputDevice import OutputDevice
from UM.OutputDevice import OutputDeviceError
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class RemovableDriveOutputDevice(OutputDevice):
def __init__(self, device_id, device_name):
super().__init__(device_id)
self.setName(device_name)
self.setShortDescription(catalog.i18nc("@action:button", "Save to Removable Drive"))
self.setDescription(catalog.i18nc("@item:inlistbox", "Save to Removable Drive {0}").format(device_name))
self.setIconName("save_sd")
self.setPriority(1)
self._writing = False
def requestWrite(self, node, file_name = None, filter_by_machine = False):
filter_by_machine = True # This plugin is indended to be used by machine (regardless of what it was told to do)
if self._writing:
raise OutputDeviceError.DeviceBusyError()
# Formats supported by this application (File types that we can actually write)
file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite()
if filter_by_machine:
container = Application.getInstance().getGlobalContainerStack().findContainer({"file_formats": "*"})
# Create a list from supported file formats string
machine_file_formats = [file_type.strip() for file_type in container.getMetaDataEntry("file_formats").split(";")]
# Take the intersection between file_formats and machine_file_formats.
file_formats = list(filter(lambda file_format: file_format["mime_type"] in machine_file_formats, file_formats))
if len(file_formats) == 0:
Logger.log("e", "There are no file formats available to write with!")
raise OutputDeviceError.WriteRequestFailedError()
# Just take the first file format available.
writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(file_formats[0]["mime_type"])
extension = file_formats[0]["extension"]
if file_name is None:
for n in BreadthFirstIterator(node):
if n.getMeshData():
file_name = n.getName()
if file_name:
break
if not file_name:
Logger.log("e", "Could not determine a proper file name when trying to write to %s, aborting", self.getName())
raise OutputDeviceError.WriteRequestFailedError()
if extension: # Not empty string.
extension = "." + extension
file_name = os.path.join(self.getId(), os.path.splitext(file_name)[0] + extension)
try:
Logger.log("d", "Writing to %s", file_name)
stream = open(file_name, "wt")
job = WriteMeshJob(writer, stream, node, MeshWriter.OutputMode.TextMode)
job.setFileName(file_name)
job.progress.connect(self._onProgress)
job.finished.connect(self._onFinished)
message = Message(catalog.i18nc("@info:progress", "Saving to Removable Drive <filename>{0}</filename>").format(self.getName()), 0, False, -1)
message.show()
self.writeStarted.emit(self)
job._message = message
self._writing = True
job.start()
except PermissionError as e:
Logger.log("e", "Permission denied when trying to write to %s: %s", file_name, str(e))
raise OutputDeviceError.PermissionDeniedError(catalog.i18nc("@info:status", "Could not save to <filename>{0}</filename>: <message>{1}</message>").format(file_name, str(e))) from e
except OSError as e:
Logger.log("e", "Operating system would not let us write to %s: %s", file_name, str(e))
raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("@info:status", "Could not save to <filename>{0}</filename>: <message>{1}</message>").format(file_name, str(e))) from e
def _onProgress(self, job, progress):
if hasattr(job, "_message"):
job._message.setProgress(progress)
self.writeProgress.emit(self, progress)
def _onFinished(self, job):
if hasattr(job, "_message"):
job._message.hide()
job._message = None
self._writing = False
self.writeFinished.emit(self)
if job.getResult():
message = Message(catalog.i18nc("@info:status", "Saved to Removable Drive {0} as {1}").format(self.getName(), os.path.basename(job.getFileName())))
message.addAction("eject", catalog.i18nc("@action:button", "Eject"), "eject", catalog.i18nc("@action", "Eject removable device {0}").format(self.getName()))
message.actionTriggered.connect(self._onActionTriggered)
message.show()
self.writeSuccess.emit(self)
else:
message = Message(catalog.i18nc("@info:status", "Could not save to removable drive {0}: {1}").format(self.getName(), str(job.getError())))
message.show()
self.writeError.emit(self)
job.getStream().close()
def _onActionTriggered(self, message, action):
if action == "eject":
Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("RemovableDriveOutputDevice").ejectDevice(self)

View file

@ -0,0 +1,80 @@
# Copyright (c) 2015 Ultimaker B.V.
# Uranium is released under the terms of the AGPLv3 or higher.
import threading
import time
from UM.Message import Message
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
from UM.Logger import Logger
from . import RemovableDriveOutputDevice
from UM.Logger import Logger
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class RemovableDrivePlugin(OutputDevicePlugin):
def __init__(self):
super().__init__()
self._update_thread = threading.Thread(target = self._updateThread)
self._update_thread.setDaemon(True)
self._check_updates = True
self._drives = {}
def start(self):
self._update_thread.start()
def stop(self):
self._check_updates = False
self._update_thread.join()
self._addRemoveDrives({})
def checkRemovableDrives(self):
raise NotImplementedError()
def ejectDevice(self, device):
try:
Logger.log("i", "Attempting to eject the device")
result = self.performEjectDevice(device)
except Exception as e:
Logger.log("e", "Ejection failed due to: %s" % str(e))
result = False
if result:
Logger.log("i", "Succesfully ejected the device")
message = Message(catalog.i18nc("@info:status", "Ejected {0}. You can now safely remove the drive.").format(device.getName()))
message.show()
else:
message = Message(catalog.i18nc("@info:status", "Failed to eject {0}. Maybe it is still in use?").format(device.getName()))
message.show()
def performEjectDevice(self, device):
raise NotImplementedError()
def _updateThread(self):
while self._check_updates:
result = self.checkRemovableDrives()
self._addRemoveDrives(result)
time.sleep(5)
def _addRemoveDrives(self, drives):
# First, find and add all new or changed keys
for key, value in drives.items():
if key not in self._drives:
self.getOutputDeviceManager().addOutputDevice(RemovableDriveOutputDevice.RemovableDriveOutputDevice(key, value))
continue
if self._drives[key] != value:
self.getOutputDeviceManager().removeOutputDevice(key)
self.getOutputDeviceManager().addOutputDevice(RemovableDriveOutputDevice.RemovableDriveOutputDevice(key, value))
# Then check for keys that have been removed
for key in self._drives.keys():
if key not in drives:
self.getOutputDeviceManager().removeOutputDevice(key)
self._drives = drives

View file

@ -0,0 +1,124 @@
# Copyright (c) 2015 Ultimaker B.V.
# Copyright (c) 2013 David Braam
# Uranium is released under the terms of the AGPLv3 or higher.
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
from . import RemovableDrivePlugin
import string
import ctypes
from ctypes import wintypes # Using ctypes.wintypes in the code below does not seem to work
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
# WinAPI Constants that we need
# Hardcoded here due to stupid WinDLL stuff that does not give us access to these values.
DRIVE_REMOVABLE = 2 # [CodeStyle: Windows Enum value]
GENERIC_READ = 2147483648 # [CodeStyle: Windows Enum value]
GENERIC_WRITE = 1073741824 # [CodeStyle: Windows Enum value]
FILE_SHARE_READ = 1 # [CodeStyle: Windows Enum value]
FILE_SHARE_WRITE = 2 # [CodeStyle: Windows Enum value]
IOCTL_STORAGE_EJECT_MEDIA = 2967560 # [CodeStyle: Windows Enum value]
OPEN_EXISTING = 3 # [CodeStyle: Windows Enum value]
# Setup the DeviceIoControl function arguments and return type.
# See ctypes documentation for details on how to call C functions from python, and why this is important.
ctypes.windll.kernel32.DeviceIoControl.argtypes = [
wintypes.HANDLE, # _In_ HANDLE hDevice
wintypes.DWORD, # _In_ DWORD dwIoControlCode
wintypes.LPVOID, # _In_opt_ LPVOID lpInBuffer
wintypes.DWORD, # _In_ DWORD nInBufferSize
wintypes.LPVOID, # _Out_opt_ LPVOID lpOutBuffer
wintypes.DWORD, # _In_ DWORD nOutBufferSize
ctypes.POINTER(wintypes.DWORD), # _Out_opt_ LPDWORD lpBytesReturned
wintypes.LPVOID # _Inout_opt_ LPOVERLAPPED lpOverlapped
]
ctypes.windll.kernel32.DeviceIoControl.restype = wintypes.BOOL
## Removable drive support for windows
class WindowsRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin):
def checkRemovableDrives(self):
drives = {}
bitmask = ctypes.windll.kernel32.GetLogicalDrives()
# Check possible drive letters, from A to Z
# Note: using ascii_uppercase because we do not want this to change with locale!
for letter in string.ascii_uppercase:
drive = "{0}:/".format(letter)
# Do we really want to skip A and B?
# GetDriveTypeA explicitly wants a byte array of type ascii. It will accept a string, but this wont work
if bitmask & 1 and ctypes.windll.kernel32.GetDriveTypeA(drive.encode("ascii")) == DRIVE_REMOVABLE:
volume_name = ""
name_buffer = ctypes.create_unicode_buffer(1024)
filesystem_buffer = ctypes.create_unicode_buffer(1024)
error = ctypes.windll.kernel32.GetVolumeInformationW(ctypes.c_wchar_p(drive), name_buffer, ctypes.sizeof(name_buffer), None, None, None, filesystem_buffer, ctypes.sizeof(filesystem_buffer))
if error != 0:
volume_name = name_buffer.value
if not volume_name:
volume_name = catalog.i18nc("@item:intext", "Removable Drive")
# Certain readers will report themselves as a volume even when there is no card inserted, but will show an
# "No volume in drive" warning when trying to call GetDiskFreeSpace. However, they will not report a valid
# filesystem, so we can filter on that. In addition, this excludes other things with filesystems Windows
# does not support.
if filesystem_buffer.value == "":
continue
# Check for the free space. Some card readers show up as a drive with 0 space free when there is no card inserted.
free_bytes = ctypes.c_longlong(0)
if ctypes.windll.kernel32.GetDiskFreeSpaceExA(drive.encode("ascii"), ctypes.byref(free_bytes), None, None) == 0:
continue
if free_bytes.value < 1:
continue
drives[drive] = "{0} ({1}:)".format(volume_name, letter)
bitmask >>= 1
return drives
def performEjectDevice(self, device):
# Magic WinAPI stuff
# First, open a handle to the Device
handle = ctypes.windll.kernel32.CreateFileA("\\\\.\\{0}".format(device.getId()[:-1]).encode("ascii"), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, None, OPEN_EXISTING, 0, None )
if handle == -1:
# ctypes.WinError sets up an GetLastError API call for windows as an Python OSError exception.
# So we use this to raise the error to our caller.
raise ctypes.WinError()
# The DeviceIoControl requires a bytes_returned pointer to be a valid pointer.
# So create a ctypes DWORD to reference. (Without this pointer the DeviceIoControl function will crash with an access violation after doing its job.
bytes_returned = wintypes.DWORD(0)
error = None
# Then, try and tell it to eject
return_code = ctypes.windll.kernel32.DeviceIoControl(handle, IOCTL_STORAGE_EJECT_MEDIA, None, 0, None, 0, ctypes.pointer(bytes_returned), None)
# DeviceIoControl with IOCTL_STORAGE_EJECT_MEDIA return 0 on error.
if return_code == 0:
# ctypes.WinError sets up an GetLastError API call for windows as an Python OSError exception.
# So we use this to raise the error to our caller.
error = ctypes.WinError()
# Do not raise an error here yet, so we can properly close the handle.
# Finally, close the handle
ctypes.windll.kernel32.CloseHandle(handle)
# If an error happened in the DeviceIoControl, raise it now.
if error:
raise error
# Return success
return True

View file

@ -0,0 +1,32 @@
# Copyright (c) 2015 Ultimaker B.V.
# Uranium is released under the terms of the AGPLv3 or higher.
import platform
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {
"plugin": {
"name": catalog.i18nc("@label", "Removable Drive Output Device Plugin"),
"author": "Ultimaker B.V.",
"description": catalog.i18nc("@info:whatsthis", "Provides removable drive hotplugging and writing support."),
"version": "1.0",
"api": 3
}
}
def register(app):
if platform.system() == "Windows":
from . import WindowsRemovableDrivePlugin
return { "output_device": WindowsRemovableDrivePlugin.WindowsRemovableDrivePlugin() }
elif platform.system() == "Darwin":
from . import OSXRemovableDrivePlugin
return { "output_device": OSXRemovableDrivePlugin.OSXRemovableDrivePlugin() }
elif platform.system() == "Linux":
from . import LinuxRemovableDrivePlugin
return { "output_device": LinuxRemovableDrivePlugin.LinuxRemovableDrivePlugin() }
else:
Logger.log("e", "Unsupported system %s, no removable device hotplugging support available.", platform.system())
return { }

View file

@ -0,0 +1,148 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from UM.Extension import Extension
from UM.Application import Application
from UM.Preferences import Preferences
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode
from UM.Message import Message
from UM.i18n import i18nCatalog
from UM.Logger import Logger
from UM.Platform import Platform
from UM.Qt.Duration import DurationFormat
from UM.Job import Job
import platform
import math
import urllib.request
import urllib.parse
import ssl
catalog = i18nCatalog("cura")
class SliceInfoJob(Job):
data = None
url = None
def __init__(self, url, data):
super().__init__()
self.url = url
self.data = data
def run(self):
if not self.url or not self.data:
Logger.log("e", "URL or DATA for sending slice info was not set!")
return
# Submit data
kwoptions = {"data" : self.data,
"timeout" : 5
}
if Platform.isOSX():
kwoptions["context"] = ssl._create_unverified_context()
try:
f = urllib.request.urlopen(self.url, **kwoptions)
Logger.log("i", "Sent anonymous slice info to %s", self.url)
f.close()
except urllib.error.HTTPError as http_exception:
Logger.log("e", "An HTTP error occurred while trying to send slice information: %s" % http_exception)
except Exception as e: # We don't want any exception to cause problems
Logger.log("e", "An exception occurred while trying to send slice information: %s" % e)
## This Extension runs in the background and sends several bits of information to the Ultimaker servers.
# The data is only sent when the user in question gave permission to do so. All data is anonymous and
# no model files are being sent (Just a SHA256 hash of the model).
class SliceInfo(Extension):
info_url = "https://stats.youmagine.com/curastats/slice"
def __init__(self):
super().__init__()
Application.getInstance().getOutputDeviceManager().writeStarted.connect(self._onWriteStarted)
Preferences.getInstance().addPreference("info/send_slice_info", True)
Preferences.getInstance().addPreference("info/asked_send_slice_info", False)
if not Preferences.getInstance().getValue("info/asked_send_slice_info"):
self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura automatically sends slice info. You can disable this in preferences"), lifetime = 0, dismissable = False)
self.send_slice_info_message.addAction("Dismiss", catalog.i18nc("@action:button", "Dismiss"), None, "")
self.send_slice_info_message.actionTriggered.connect(self.messageActionTriggered)
self.send_slice_info_message.show()
def messageActionTriggered(self, message_id, action_id):
self.send_slice_info_message.hide()
Preferences.getInstance().setValue("info/asked_send_slice_info", True)
def _onWriteStarted(self, output_device):
try:
if not Preferences.getInstance().getValue("info/send_slice_info"):
Logger.log("d", "'info/send_slice_info' is turned off.")
return # Do nothing, user does not want to send data
global_container_stack = Application.getInstance().getGlobalContainerStack()
# Get total material used (in mm^3)
print_information = Application.getInstance().getPrintInformation()
material_radius = 0.5 * global_container_stack.getProperty("material_diameter", "value")
# TODO: Send material per extruder instead of mashing it on a pile
material_used = math.pi * material_radius * material_radius * sum(print_information.materialLengths) #Volume of all materials used
# Get model information (bounding boxes, hashes and transformation matrix)
models_info = []
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
if not getattr(node, "_outside_buildarea", False):
model_info = {}
model_info["hash"] = node.getMeshData().getHash()
model_info["bounding_box"] = {}
model_info["bounding_box"]["minimum"] = {}
model_info["bounding_box"]["minimum"]["x"] = node.getBoundingBox().minimum.x
model_info["bounding_box"]["minimum"]["y"] = node.getBoundingBox().minimum.y
model_info["bounding_box"]["minimum"]["z"] = node.getBoundingBox().minimum.z
model_info["bounding_box"]["maximum"] = {}
model_info["bounding_box"]["maximum"]["x"] = node.getBoundingBox().maximum.x
model_info["bounding_box"]["maximum"]["y"] = node.getBoundingBox().maximum.y
model_info["bounding_box"]["maximum"]["z"] = node.getBoundingBox().maximum.z
model_info["transformation"] = str(node.getWorldTransformation().getData())
models_info.append(model_info)
# Bundle the collected data
submitted_data = {
"processor": platform.processor(),
"machine": platform.machine(),
"platform": platform.platform(),
"settings": global_container_stack.serialize(), # global_container with references on used containers
"version": Application.getInstance().getVersion(),
"modelhash": "None",
"printtime": print_information.currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601),
"filament": material_used,
"language": Preferences.getInstance().getValue("general/language"),
}
for container in global_container_stack.getContainers():
container_id = container.getId()
try:
container_serialized = container.serialize()
except NotImplementedError:
Logger.log("w", "Container %s could not be serialized!", container_id)
continue
if container_serialized:
submitted_data["settings_%s" %(container_id)] = container_serialized # This can be anything, eg. INI, JSON, etc.
else:
Logger.log("i", "No data found in %s to be serialized!", container_id)
# Convert data to bytes
submitted_data = urllib.parse.urlencode(submitted_data)
binary_data = submitted_data.encode("utf-8")
# Sending slice info non-blocking
reportJob = SliceInfoJob(self.info_url, binary_data)
reportJob.start()
except Exception as e:
# We really can't afford to have a mistake here, as this would break the sending of g-code to a device
# (Either saving or directly to a printer). The functionality of the slice data is not *that* important.
Logger.log("e", "Exception raised while sending slice info: %s" %(repr(e))) # But we should be notified about these problems of course.

View file

@ -0,0 +1,19 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from . import SliceInfo
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {
"plugin": {
"name": catalog.i18nc("@label", "Slice info"),
"author": "Ultimaker",
"version": "1.0",
"description": catalog.i18nc("@info:whatsthis", "Submits anonymous slice info. Can be disabled through preferences."),
"api": 3
}
}
def register(app):
return { "extension": SliceInfo.SliceInfo()}

View file

@ -0,0 +1,105 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from UM.View.View import View
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.Selection import Selection
from UM.Resources import Resources
from UM.Application import Application
from UM.Preferences import Preferences
from UM.View.Renderer import Renderer
from UM.Settings.Validator import ValidatorState
from UM.View.GL.OpenGL import OpenGL
import cura.Settings
import math
## Standard view for mesh models.
class SolidView(View):
def __init__(self):
super().__init__()
Preferences.getInstance().addPreference("view/show_overhang", True)
self._enabled_shader = None
self._disabled_shader = None
self._extruders_model = cura.Settings.ExtrudersModel()
def beginRendering(self):
scene = self.getController().getScene()
renderer = self.getRenderer()
if not self._enabled_shader:
self._enabled_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "overhang.shader"))
if not self._disabled_shader:
self._disabled_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "striped.shader"))
self._disabled_shader.setUniformValue("u_diffuseColor1", [0.48, 0.48, 0.48, 1.0])
self._disabled_shader.setUniformValue("u_diffuseColor2", [0.68, 0.68, 0.68, 1.0])
self._disabled_shader.setUniformValue("u_width", 50.0)
multi_extrusion = False
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
if Preferences.getInstance().getValue("view/show_overhang"):
angle = global_container_stack.getProperty("support_angle", "value")
if angle is not None and global_container_stack.getProperty("support_angle", "validationState") == ValidatorState.Valid:
self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(90 - angle)))
else:
self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0))) #Overhang angle of 0 causes no area at all to be marked as overhang.
else:
self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0)))
multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
for node in DepthFirstIterator(scene.getRoot()):
if not node.render(renderer):
if node.getMeshData() and node.isVisible():
uniforms = {}
if not multi_extrusion:
if global_container_stack:
material = global_container_stack.findContainer({ "type": "material" })
material_color = material.getMetaDataEntry("color_code", default = self._extruders_model.defaultColors[0]) if material else self._extruders_model.defaultColors[0]
else:
material_color = self._extruders_model.defaultColors[0]
else:
# Get color to render this mesh in from ExtrudersModel
extruder_index = 0
extruder_id = node.callDecoration("getActiveExtruder")
if extruder_id:
extruder_index = max(0, self._extruders_model.find("id", extruder_id))
material_color = self._extruders_model.getItem(extruder_index)["color"]
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,
1.0
]
except ValueError:
pass
if hasattr(node, "_outside_buildarea"):
if node._outside_buildarea:
renderer.queueNode(node, shader = self._disabled_shader)
else:
renderer.queueNode(node, shader = self._enabled_shader, uniforms = uniforms)
else:
renderer.queueNode(node, material = self._enabled_shader, uniforms = uniforms)
if node.callDecoration("isGroup") and Selection.isSelected(node):
renderer.queueNode(scene.getRoot(), mesh = node.getBoundingBoxMesh(), mode = Renderer.RenderLines)
def endRendering(self):
pass
#def _onPreferenceChanged(self, preference):
#if preference == "view/show_overhang": ## Todo: This a printer only setting. Should be removed from Uranium.
#self._enabled_material = None

View file

@ -0,0 +1,25 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from . import SolidView
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
def getMetaData():
return {
"plugin": {
"name": i18n_catalog.i18nc("@label", "Solid View"),
"author": "Ultimaker",
"version": "1.0",
"description": i18n_catalog.i18nc("@info:whatsthis", "Provides a normal solid mesh view."),
"api": 3
},
"view": {
"name": i18n_catalog.i18nc("@item:inmenu", "Solid"),
"weight": 0
}
}
def register(app):
return { "view": SolidView.SolidView() }

View file

@ -0,0 +1,86 @@
// Copyright (c) 2015 Ultimaker B.V.
// Cura is released under the terms of the AGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Window 2.2
import QtQuick.Controls 1.2
import UM 1.1 as UM
UM.Dialog
{
id: base;
width: 500 * Screen.devicePixelRatio;
height: 100 * Screen.devicePixelRatio;
visible: true;
modality: Qt.ApplicationModal;
title: catalog.i18nc("@title:window","Firmware Update");
Column
{
anchors.fill: parent;
Label
{
anchors
{
left: parent.left;
right: parent.right;
}
text: {
if (manager.firmwareUpdateCompleteStatus)
{
//: Firmware update status label
return catalog.i18nc("@label","Firmware update completed.")
}
else if (manager.progress == 0)
{
//: Firmware update status label
return catalog.i18nc("@label","Starting firmware update, this may take a while.")
}
else
{
//: Firmware update status label
return catalog.i18nc("@label","Updating firmware.")
}
}
wrapMode: Text.Wrap;
}
ProgressBar
{
id: prog
value: manager.firmwareUpdateCompleteStatus ? 100 : manager.progress
minimumValue: 0
maximumValue: 100
indeterminate: (manager.progress < 1) && (!manager.firmwareUpdateCompleteStatus)
anchors
{
left: parent.left;
right: parent.right;
}
}
SystemPalette
{
id: palette;
}
UM.I18nCatalog { id: catalog; name: "cura"; }
}
rightButtons: [
Button
{
text: catalog.i18nc("@action:button","Close");
enabled: manager.firmwareUpdateCompleteStatus;
onClicked: base.visible = false;
}
]
}

View file

@ -0,0 +1,583 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from .avr_isp import stk500v2, ispBase, intelHex
import serial
import threading
import time
import queue
import re
import functools
from UM.Application import Application
from UM.Logger import Logger
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
from UM.Message import Message
from PyQt5.QtCore import QUrl, pyqtSlot, pyqtSignal
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class USBPrinterOutputDevice(PrinterOutputDevice):
def __init__(self, serial_port):
super().__init__(serial_port)
self.setName(catalog.i18nc("@item:inmenu", "USB printing"))
self.setShortDescription(catalog.i18nc("@action:button", "Print via USB"))
self.setDescription(catalog.i18nc("@info:tooltip", "Print via USB"))
self.setIconName("print")
self.setConnectionText(catalog.i18nc("@info:status", "Connected via USB"))
self._serial = None
self._serial_port = serial_port
self._error_state = None
self._connect_thread = threading.Thread(target = self._connect)
self._connect_thread.daemon = True
self._end_stop_thread = None
self._poll_endstop = False
# The baud checking is done by sending a number of m105 commands to the printer and waiting for a readable
# response. If the baudrate is correct, this should make sense, else we get giberish.
self._required_responses_auto_baud = 3
self._listen_thread = threading.Thread(target=self._listen)
self._listen_thread.daemon = True
self._update_firmware_thread = threading.Thread(target= self._updateFirmware)
self._update_firmware_thread.daemon = True
self.firmwareUpdateComplete.connect(self._onFirmwareUpdateComplete)
self._heatup_wait_start_time = time.time()
## Queue for commands that need to be send. Used when command is sent when a print is active.
self._command_queue = queue.Queue()
self._is_printing = False
self._is_paused = False
## Set when print is started in order to check running time.
self._print_start_time = None
self._print_start_time_100 = None
## Keep track where in the provided g-code the print is
self._gcode_position = 0
# List of gcode lines to be printed
self._gcode = []
# Check if endstops are ever pressed (used for first run)
self._x_min_endstop_pressed = False
self._y_min_endstop_pressed = False
self._z_min_endstop_pressed = False
self._x_max_endstop_pressed = False
self._y_max_endstop_pressed = False
self._z_max_endstop_pressed = False
# In order to keep the connection alive we request the temperature every so often from a different extruder.
# This index is the extruder we requested data from the last time.
self._temperature_requested_extruder_index = 0
self._current_z = 0
self._updating_firmware = False
self._firmware_file_name = None
self._firmware_update_finished = False
self._error_message = None
onError = pyqtSignal()
firmwareUpdateComplete = pyqtSignal()
firmwareUpdateChange = pyqtSignal()
endstopStateChanged = pyqtSignal(str ,bool, arguments = ["key","state"])
def _setTargetBedTemperature(self, temperature):
Logger.log("d", "Setting bed temperature to %s", temperature)
self._sendCommand("M140 S%s" % temperature)
def _setTargetHotendTemperature(self, index, temperature):
Logger.log("d", "Setting hotend %s temperature to %s", index, temperature)
self._sendCommand("M104 T%s S%s" % (index, temperature))
def _setHeadPosition(self, x, y , z, speed):
self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))
def _setHeadX(self, x, speed):
self._sendCommand("G0 X%s F%s" % (x, speed))
def _setHeadY(self, y, speed):
self._sendCommand("G0 Y%s F%s" % (y, speed))
def _setHeadZ(self, z, speed):
self._sendCommand("G0 Y%s F%s" % (z, speed))
def _homeHead(self):
self._sendCommand("G28")
def _homeBed(self):
self._sendCommand("G28 Z")
def startPrint(self):
self.writeStarted.emit(self)
gcode_list = getattr( Application.getInstance().getController().getScene(), "gcode_list")
self._updateJobState("printing")
self.printGCode(gcode_list)
def _moveHead(self, x, y, z, speed):
self._sendCommand("G91")
self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))
self._sendCommand("G90")
## Start a print based on a g-code.
# \param gcode_list List with gcode (strings).
def printGCode(self, gcode_list):
if self._progress or self._connection_state != ConnectionState.connected:
self._error_message = Message(catalog.i18nc("@info:status", "Printer is busy or not connected. Unable to start a new job."))
self._error_message.show()
Logger.log("d", "Printer is busy or not connected, aborting print")
self.writeError.emit(self)
return
self._gcode.clear()
for layer in gcode_list:
self._gcode.extend(layer.split("\n"))
# Reset line number. If this is not done, first line is sometimes ignored
self._gcode.insert(0, "M110")
self._gcode_position = 0
self._print_start_time_100 = None
self._is_printing = True
self._print_start_time = time.time()
for i in range(0, 4): # Push first 4 entries before accepting other inputs
self._sendNextGcodeLine()
self.writeFinished.emit(self)
## Get the serial port string of this connection.
# \return serial port
def getSerialPort(self):
return self._serial_port
## Try to connect the serial. This simply starts the thread, which runs _connect.
def connect(self):
if not self._updating_firmware and not self._connect_thread.isAlive():
self._connect_thread.start()
## Private function (threaded) that actually uploads the firmware.
def _updateFirmware(self):
self.setProgress(0, 100)
self._firmware_update_finished = False
if self._connection_state != ConnectionState.closed:
self.close()
hex_file = intelHex.readHex(self._firmware_file_name)
if len(hex_file) == 0:
Logger.log("e", "Unable to read provided hex file. Could not update firmware")
self._updateFirmware_completed()
return
programmer = stk500v2.Stk500v2()
programmer.progress_callback = self.setProgress
try:
programmer.connect(self._serial_port)
except Exception:
pass
# Give programmer some time to connect. Might need more in some cases, but this worked in all tested cases.
time.sleep(1)
if not programmer.isConnected():
Logger.log("e", "Unable to connect with serial. Could not update firmware")
self._updateFirmware_completed()
return
self._updating_firmware = True
try:
programmer.programChip(hex_file)
self._updating_firmware = False
except Exception as e:
Logger.log("e", "Exception while trying to update firmware %s" %e)
self._updateFirmware_completed()
return
programmer.close()
self._updateFirmware_completed()
return
## Private function which makes sure that firmware update process has completed/ended
def _updateFirmware_completed(self):
self.setProgress(100, 100)
self._firmware_update_finished = True
self.resetFirmwareUpdate(update_has_finished=True)
self.firmwareUpdateComplete.emit()
return
## Upload new firmware to machine
# \param filename full path of firmware file to be uploaded
def updateFirmware(self, file_name):
Logger.log("i", "Updating firmware of %s using %s", self._serial_port, file_name)
self._firmware_file_name = file_name
self._update_firmware_thread.start()
@property
def firmwareUpdateFinished(self):
return self._firmware_update_finished
def resetFirmwareUpdate(self, update_has_finished = False):
self._firmware_update_finished = update_has_finished
self.firmwareUpdateChange.emit()
@pyqtSlot()
def startPollEndstop(self):
if not self._poll_endstop:
self._poll_endstop = True
if self._end_stop_thread is None:
self._end_stop_thread = threading.Thread(target=self._pollEndStop)
self._end_stop_thread.daemon = True
self._end_stop_thread.start()
@pyqtSlot()
def stopPollEndstop(self):
self._poll_endstop = False
self._end_stop_thread = None
def _pollEndStop(self):
while self._connection_state == ConnectionState.connected and self._poll_endstop:
self.sendCommand("M119")
time.sleep(0.5)
## Private connect function run by thread. Can be started by calling connect.
def _connect(self):
Logger.log("d", "Attempting to connect to %s", self._serial_port)
self.setConnectionState(ConnectionState.connecting)
programmer = stk500v2.Stk500v2()
try:
programmer.connect(self._serial_port) # Connect with the serial, if this succeeds, it's an arduino based usb device.
self._serial = programmer.leaveISP()
except ispBase.IspError as e:
Logger.log("i", "Could not establish connection on %s: %s. Device is not arduino based." %(self._serial_port,str(e)))
except Exception as e:
Logger.log("i", "Could not establish connection on %s, unknown reasons. Device is not arduino based." % self._serial_port)
# If the programmer connected, we know its an atmega based version.
# Not all that useful, but it does give some debugging information.
for baud_rate in self._getBaudrateList(): # Cycle all baud rates (auto detect)
Logger.log("d", "Attempting to connect to printer with serial %s on baud rate %s", self._serial_port, baud_rate)
if self._serial is None:
try:
self._serial = serial.Serial(str(self._serial_port), baud_rate, timeout = 3, writeTimeout = 10000)
except serial.SerialException:
Logger.log("d", "Could not open port %s" % self._serial_port)
continue
else:
if not self.setBaudRate(baud_rate):
continue # Could not set the baud rate, go to the next
time.sleep(1.5) # Ensure that we are not talking to the bootloader. 1.5 seconds seems to be the magic number
sucesfull_responses = 0
timeout_time = time.time() + 5
self._serial.write(b"\n")
self._sendCommand("M105") # Request temperature, as this should (if baudrate is correct) result in a command with "T:" in it
while timeout_time > time.time():
line = self._readline()
if line is None:
Logger.log("d", "No response from serial connection received.")
# Something went wrong with reading, could be that close was called.
self.setConnectionState(ConnectionState.closed)
return
if b"T:" in line:
Logger.log("d", "Correct response for auto-baudrate detection received.")
self._serial.timeout = 0.5
sucesfull_responses += 1
if sucesfull_responses >= self._required_responses_auto_baud:
self._serial.timeout = 2 # Reset serial timeout
self.setConnectionState(ConnectionState.connected)
self._listen_thread.start() # Start listening
Logger.log("i", "Established printer connection on port %s" % self._serial_port)
return
self._sendCommand("M105") # Send M105 as long as we are listening, otherwise we end up in an undefined state
Logger.log("e", "Baud rate detection for %s failed", self._serial_port)
self.close() # Unable to connect, wrap up.
self.setConnectionState(ConnectionState.closed)
## Set the baud rate of the serial. This can cause exceptions, but we simply want to ignore those.
def setBaudRate(self, baud_rate):
try:
self._serial.baudrate = baud_rate
return True
except Exception as e:
return False
## Close the printer connection
def close(self):
Logger.log("d", "Closing the USB printer connection.")
if self._connect_thread.isAlive():
try:
self._connect_thread.join()
except Exception as e:
Logger.log("d", "PrinterConnection.close: %s (expected)", e)
pass # This should work, but it does fail sometimes for some reason
self._connect_thread = threading.Thread(target = self._connect)
self._connect_thread.daemon = True
self.setConnectionState(ConnectionState.closed)
if self._serial is not None:
try:
self._listen_thread.join()
except:
pass
self._serial.close()
self._listen_thread = threading.Thread(target = self._listen)
self._listen_thread.daemon = True
self._serial = None
## Directly send the command, withouth checking connection state (eg; printing).
# \param cmd string with g-code
def _sendCommand(self, cmd):
if self._serial is None:
return
if "M109" in cmd or "M190" in cmd:
self._heatup_wait_start_time = time.time()
try:
command = (cmd + "\n").encode()
self._serial.write(b"\n")
self._serial.write(command)
except serial.SerialTimeoutException:
Logger.log("w","Serial timeout while writing to serial port, trying again.")
try:
time.sleep(0.5)
self._serial.write((cmd + "\n").encode())
except Exception as e:
Logger.log("e","Unexpected error while writing serial port %s " % e)
self._setErrorState("Unexpected error while writing serial port %s " % e)
self.close()
except Exception as e:
Logger.log("e","Unexpected error while writing serial port %s" % e)
self._setErrorState("Unexpected error while writing serial port %s " % e)
self.close()
## Send a command to printer.
# \param cmd string with g-code
def sendCommand(self, cmd):
if self._progress:
self._command_queue.put(cmd)
elif self._connection_state == ConnectionState.connected:
self._sendCommand(cmd)
## Set the error state with a message.
# \param error String with the error message.
def _setErrorState(self, error):
self._updateJobState("error")
self._error_state = error
self.onError.emit()
def requestWrite(self, node, file_name = None, filter_by_machine = False):
Application.getInstance().showPrintMonitor.emit(True)
self.startPrint()
def _setEndstopState(self, endstop_key, value):
if endstop_key == b"x_min":
if self._x_min_endstop_pressed != value:
self.endstopStateChanged.emit("x_min", value)
self._x_min_endstop_pressed = value
elif endstop_key == b"y_min":
if self._y_min_endstop_pressed != value:
self.endstopStateChanged.emit("y_min", value)
self._y_min_endstop_pressed = value
elif endstop_key == b"z_min":
if self._z_min_endstop_pressed != value:
self.endstopStateChanged.emit("z_min", value)
self._z_min_endstop_pressed = value
## Listen thread function.
def _listen(self):
Logger.log("i", "Printer connection listen thread started for %s" % self._serial_port)
temperature_request_timeout = time.time()
ok_timeout = time.time()
while self._connection_state == ConnectionState.connected:
line = self._readline()
if line is None:
break # None is only returned when something went wrong. Stop listening
if time.time() > temperature_request_timeout:
if self._num_extruders > 0:
self._temperature_requested_extruder_index = (self._temperature_requested_extruder_index + 1) % self._num_extruders
self.sendCommand("M105 T%d" % (self._temperature_requested_extruder_index))
else:
self.sendCommand("M105")
temperature_request_timeout = time.time() + 5
if line.startswith(b"Error:"):
# Oh YEAH, consistency.
# Marlin reports a MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
# But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
# So we can have an extra newline in the most common case. Awesome work people.
if re.match(b"Error:[0-9]\n", line):
line = line.rstrip() + self._readline()
# Skip the communication errors, as those get corrected.
if b"Extruder switched off" in line or b"Temperature heated bed switched off" in line or b"Something is wrong, please turn off the printer." in line:
if not self.hasError():
self._setErrorState(line[6:])
elif b" T:" in line or line.startswith(b"T:"): # Temperature message
try:
self._setHotendTemperature(self._temperature_requested_extruder_index, float(re.search(b"T: *([0-9\.]*)", line).group(1)))
except:
pass
if b"B:" in line: # Check if it's a bed temperature
try:
self._setBedTemperature(float(re.search(b"B: *([0-9\.]*)", line).group(1)))
except Exception as e:
pass
#TODO: temperature changed callback
elif b"_min" in line or b"_max" in line:
tag, value = line.split(b":", 1)
self._setEndstopState(tag,(b"H" in value or b"TRIGGERED" in value))
if self._is_printing:
if line == b"" and time.time() > ok_timeout:
line = b"ok" # Force a timeout (basically, send next command)
if b"ok" in line:
ok_timeout = time.time() + 5
if not self._command_queue.empty():
self._sendCommand(self._command_queue.get())
elif self._is_paused:
line = b"" # Force getting temperature as keep alive
else:
self._sendNextGcodeLine()
elif b"resend" in line.lower() or b"rs" in line: # Because a resend can be asked with "resend" and "rs"
try:
self._gcode_position = int(line.replace(b"N:",b" ").replace(b"N",b" ").replace(b":",b" ").split()[-1])
except:
if b"rs" in line:
self._gcode_position = int(line.split()[1])
# Request the temperature on comm timeout (every 2 seconds) when we are not printing.)
if line == b"":
if self._num_extruders > 0:
self._temperature_requested_extruder_index = (self._temperature_requested_extruder_index + 1) % self._num_extruders
self.sendCommand("M105 T%d" % self._temperature_requested_extruder_index)
else:
self.sendCommand("M105")
Logger.log("i", "Printer connection listen thread stopped for %s" % self._serial_port)
## Send next Gcode in the gcode list
def _sendNextGcodeLine(self):
if self._gcode_position >= len(self._gcode):
return
if self._gcode_position == 100:
self._print_start_time_100 = time.time()
line = self._gcode[self._gcode_position]
if ";" in line:
line = line[:line.find(";")]
line = line.strip()
try:
if line == "M0" or line == "M1":
line = "M105" # Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
if ("G0" in line or "G1" in line) and "Z" in line:
z = float(re.search("Z([0-9\.]*)", line).group(1))
if self._current_z != z:
self._current_z = z
except Exception as e:
Logger.log("e", "Unexpected error with printer connection: %s" % e)
self._setErrorState("Unexpected error: %s" %e)
checksum = functools.reduce(lambda x,y: x^y, map(ord, "N%d%s" % (self._gcode_position, line)))
self._sendCommand("N%d%s*%d" % (self._gcode_position, line, checksum))
self._gcode_position += 1
self.setProgress((self._gcode_position / len(self._gcode)) * 100)
self.progressChanged.emit()
## Set the state of the print.
# Sent from the print monitor
def _setJobState(self, job_state):
if job_state == "pause":
self._is_paused = True
self._updateJobState("paused")
elif job_state == "print":
self._is_paused = False
self._updateJobState("printing")
elif job_state == "abort":
self.cancelPrint()
## Set the progress of the print.
# It will be normalized (based on max_progress) to range 0 - 100
def setProgress(self, progress, max_progress = 100):
self._progress = (progress / max_progress) * 100 # Convert to scale of 0-100
if self._progress == 100:
# Printing is done, reset progress
self._gcode_position = 0
self.setProgress(0)
self._is_printing = False
self._is_paused = False
self._updateJobState("ready")
self.progressChanged.emit()
## Cancel the current print. Printer connection wil continue to listen.
def cancelPrint(self):
self._gcode_position = 0
self.setProgress(0)
self._gcode = []
# Turn off temperatures, fan and steppers
self._sendCommand("M140 S0")
self._sendCommand("M104 S0")
self._sendCommand("M107")
self._sendCommand("M84")
self._is_printing = False
self._is_paused = False
self._updateJobState("ready")
Application.getInstance().showPrintMonitor.emit(False)
## Check if the process did not encounter an error yet.
def hasError(self):
return self._error_state is not None
## private read line used by printer connection to listen for data on serial port.
def _readline(self):
if self._serial is None:
return None
try:
ret = self._serial.readline()
except Exception as e:
Logger.log("e", "Unexpected error while reading serial port. %s" % e)
self._setErrorState("Printer has been disconnected")
self.close()
return None
return ret
## Create a list of baud rates at which we can communicate.
# \return list of int
def _getBaudrateList(self):
ret = [115200, 250000, 230400, 57600, 38400, 19200, 9600]
return ret
def _onFirmwareUpdateComplete(self):
self._update_firmware_thread.join()
self._update_firmware_thread = threading.Thread(target = self._updateFirmware)
self._update_firmware_thread.daemon = True
self.connect()

View file

@ -0,0 +1,270 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from UM.Signal import Signal, signalemitter
from . import USBPrinterOutputDevice
from UM.Application import Application
from UM.Resources import Resources
from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
from cura.PrinterOutputDevice import ConnectionState
from UM.Qt.ListModel import ListModel
from UM.Message import Message
from cura.CuraApplication import CuraApplication
import threading
import platform
import glob
import time
import os.path
from UM.Extension import Extension
from PyQt5.QtQml import QQmlComponent, QQmlContext
from PyQt5.QtCore import QUrl, QObject, pyqtSlot, pyqtProperty, pyqtSignal, Qt
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
## Manager class that ensures that a usbPrinteroutput device is created for every connected USB printer.
@signalemitter
class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
def __init__(self, parent = None):
super().__init__(parent = parent)
self._serial_port_list = []
self._usb_output_devices = {}
self._usb_output_devices_model = None
self._update_thread = threading.Thread(target = self._updateThread)
self._update_thread.setDaemon(True)
self._check_updates = True
self._firmware_view = None
Application.getInstance().applicationShuttingDown.connect(self.stop)
self.addUSBOutputDeviceSignal.connect(self.addOutputDevice) #Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
addUSBOutputDeviceSignal = Signal()
connectionStateChanged = pyqtSignal()
progressChanged = pyqtSignal()
firmwareUpdateChange = pyqtSignal()
@pyqtProperty(float, notify = progressChanged)
def progress(self):
progress = 0
for printer_name, device in self._usb_output_devices.items(): # TODO: @UnusedVariable "printer_name"
progress += device.progress
return progress / len(self._usb_output_devices)
## Return True if all printers finished firmware update
@pyqtProperty(float, notify = firmwareUpdateChange)
def firmwareUpdateCompleteStatus(self):
complete = True
for printer_name, device in self._usb_output_devices.items(): # TODO: @UnusedVariable "printer_name"
if not device.firmwareUpdateFinished:
complete = False
return complete
def start(self):
self._check_updates = True
self._update_thread.start()
def stop(self):
self._check_updates = False
try:
self._update_thread.join()
except RuntimeError:
pass
def _updateThread(self):
while self._check_updates:
result = self.getSerialPortList(only_list_usb = True)
self._addRemovePorts(result)
time.sleep(5)
## Show firmware interface.
# This will create the view if its not already created.
def spawnFirmwareInterface(self, serial_port):
if self._firmware_view is None:
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("USBPrinting"), "FirmwareUpdateWindow.qml"))
component = QQmlComponent(Application.getInstance()._engine, path)
self._firmware_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._firmware_context.setContextProperty("manager", self)
self._firmware_view = component.create(self._firmware_context)
self._firmware_view.show()
@pyqtSlot()
def updateAllFirmware(self):
if not self._usb_output_devices:
Message(i18n_catalog.i18nc("@info","Cannot update firmware, there were no connected printers found.")).show()
return
for printer_connection in self._usb_output_devices:
self._usb_output_devices[printer_connection].resetFirmwareUpdate()
self.spawnFirmwareInterface("")
for printer_connection in self._usb_output_devices:
try:
self._usb_output_devices[printer_connection].updateFirmware(Resources.getPath(CuraApplication.ResourceTypes.Firmware, self._getDefaultFirmwareName()))
except FileNotFoundError:
# Should only happen in dev environments where the resources/firmware folder is absent.
self._usb_output_devices[printer_connection].setProgress(100, 100)
Logger.log("w", "No firmware found for printer %s called '%s'" %(printer_connection, self._getDefaultFirmwareName()))
Message(i18n_catalog.i18nc("@info",
"Could not find firmware required for the printer at %s.") % printer_connection).show()
self._firmware_view.close()
continue
@pyqtSlot(str, result = bool)
def updateFirmwareBySerial(self, serial_port):
if serial_port in self._usb_output_devices:
self.spawnFirmwareInterface(self._usb_output_devices[serial_port].getSerialPort())
try:
self._usb_output_devices[serial_port].updateFirmware(Resources.getPath(CuraApplication.ResourceTypes.Firmware, self._getDefaultFirmwareName()))
except FileNotFoundError:
self._firmware_view.close()
Logger.log("e", "Could not find firmware required for this machine called '%s'" %(self._getDefaultFirmwareName()))
return False
return True
return False
## Return the singleton instance of the USBPrinterManager
@classmethod
def getInstance(cls, engine = None, script_engine = None):
# Note: Explicit use of class name to prevent issues with inheritance.
if USBPrinterOutputDeviceManager._instance is None:
USBPrinterOutputDeviceManager._instance = cls()
return USBPrinterOutputDeviceManager._instance
def _getDefaultFirmwareName(self):
# Check if there is a valid global container stack
global_container_stack = Application.getInstance().getGlobalContainerStack()
if not global_container_stack:
Logger.log("e", "There is no global container stack. Can not update firmware.")
self._firmware_view.close()
return ""
# The bottom of the containerstack is the machine definition
machine_id = global_container_stack.getBottom().id
machine_has_heated_bed = global_container_stack.getProperty("machine_heated_bed", "value")
if platform.system() == "Linux":
baudrate = 115200
else:
baudrate = 250000
# NOTE: The keyword used here is the id of the machine. You can find the id of your machine in the *.json file, eg.
# https://github.com/Ultimaker/Cura/blob/master/resources/machines/ultimaker_original.json#L2
# The *.hex files are stored at a seperate repository:
# https://github.com/Ultimaker/cura-binary-data/tree/master/cura/resources/firmware
machine_without_extras = {"bq_witbox" : "MarlinWitbox.hex",
"bq_hephestos_2" : "MarlinHephestos2.hex",
"ultimaker_original" : "MarlinUltimaker-{baudrate}.hex",
"ultimaker_original_plus" : "MarlinUltimaker-UMOP-{baudrate}.hex",
"ultimaker_original_dual" : "MarlinUltimaker-{baudrate}-dual.hex",
"ultimaker2" : "MarlinUltimaker2.hex",
"ultimaker2_go" : "MarlinUltimaker2go.hex",
"ultimaker2_plus" : "MarlinUltimaker2plus.hex",
"ultimaker2_extended" : "MarlinUltimaker2extended.hex",
"ultimaker2_extended_plus" : "MarlinUltimaker2extended-plus.hex",
}
machine_with_heated_bed = {"ultimaker_original" : "MarlinUltimaker-HBK-{baudrate}.hex",
"ultimaker_original_dual" : "MarlinUltimaker-HBK-{baudrate}-dual.hex",
}
##TODO: Add check for multiple extruders
hex_file = None
if machine_id in machine_without_extras.keys(): # The machine needs to be defined here!
if machine_id in machine_with_heated_bed.keys() and machine_has_heated_bed:
Logger.log("d", "Choosing firmware with heated bed enabled for machine %s.", machine_id)
hex_file = machine_with_heated_bed[machine_id] # Return firmware with heated bed enabled
else:
Logger.log("d", "Choosing basic firmware for machine %s.", machine_id)
hex_file = machine_without_extras[machine_id] # Return "basic" firmware
else:
Logger.log("e", "There is no firmware for machine %s.", machine_id)
if hex_file:
return hex_file.format(baudrate=baudrate)
else:
Logger.log("e", "Could not find any firmware for machine %s.", machine_id)
raise FileNotFoundError()
## Helper to identify serial ports (and scan for them)
def _addRemovePorts(self, serial_ports):
# First, find and add all new or changed keys
for serial_port in list(serial_ports):
if serial_port not in self._serial_port_list:
self.addUSBOutputDeviceSignal.emit(serial_port) # Hack to ensure its created in main thread
continue
self._serial_port_list = list(serial_ports)
devices_to_remove = []
for port, device in self._usb_output_devices.items():
if port not in self._serial_port_list:
device.close()
devices_to_remove.append(port)
for port in devices_to_remove:
del self._usb_output_devices[port]
## Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
def addOutputDevice(self, serial_port):
device = USBPrinterOutputDevice.USBPrinterOutputDevice(serial_port)
device.connectionStateChanged.connect(self._onConnectionStateChanged)
device.connect()
device.progressChanged.connect(self.progressChanged)
device.firmwareUpdateChange.connect(self.firmwareUpdateChange)
self._usb_output_devices[serial_port] = device
## If one of the states of the connected devices change, we might need to add / remove them from the global list.
def _onConnectionStateChanged(self, serial_port):
try:
if self._usb_output_devices[serial_port].connectionState == ConnectionState.connected:
self.getOutputDeviceManager().addOutputDevice(self._usb_output_devices[serial_port])
else:
self.getOutputDeviceManager().removeOutputDevice(serial_port)
self.connectionStateChanged.emit()
except KeyError:
pass # no output device by this device_id found in connection list.
@pyqtProperty(QObject , notify = connectionStateChanged)
def connectedPrinterList(self):
self._usb_output_devices_model = ListModel()
self._usb_output_devices_model.addRoleName(Qt.UserRole + 1, "name")
self._usb_output_devices_model.addRoleName(Qt.UserRole + 2, "printer")
for connection in self._usb_output_devices:
if self._usb_output_devices[connection].connectionState == ConnectionState.connected:
self._usb_output_devices_model.appendItem({"name": connection, "printer": self._usb_output_devices[connection]})
return self._usb_output_devices_model
## Create a list of serial ports on the system.
# \param only_list_usb If true, only usb ports are listed
def getSerialPortList(self, only_list_usb = False):
base_list = []
if platform.system() == "Windows":
import winreg #@UnresolvedImport
try:
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,"HARDWARE\\DEVICEMAP\\SERIALCOMM")
i = 0
while True:
values = winreg.EnumValue(key, i)
if not only_list_usb or "USBSER" in values[0]:
base_list += [values[1]]
i += 1
except Exception as e:
pass
else:
if only_list_usb:
base_list = base_list + glob.glob("/dev/ttyUSB*") + glob.glob("/dev/ttyACM*") + glob.glob("/dev/cu.usb*")
base_list = filter(lambda s: "Bluetooth" not in s, base_list) # Filter because mac sometimes puts them in the list
else:
base_list = base_list + glob.glob("/dev/ttyUSB*") + glob.glob("/dev/ttyACM*") + glob.glob("/dev/cu.*") + glob.glob("/dev/tty.usb*") + glob.glob("/dev/rfcomm*") + glob.glob("/dev/serial/by-id/*")
return list(base_list)
_instance = None

View file

@ -0,0 +1,25 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from . import USBPrinterOutputDeviceManager
from PyQt5.QtQml import qmlRegisterType, qmlRegisterSingletonType
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
def getMetaData():
return {
"type": "extension",
"plugin": {
"name": i18n_catalog.i18nc("@label", "USB printing"),
"author": "Ultimaker",
"version": "1.0",
"api": 3,
"description": i18n_catalog.i18nc("@info:whatsthis","Accepts G-Code and sends them to a printer. Plugin can also update firmware.")
}
}
def register(app):
# We are violating the QT API here (as we use a factory, which is technically not allowed).
# but we don't really have another means for doing this (and it seems to you know -work-)
qmlRegisterSingletonType(USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager, "Cura", 1, 0, "USBPrinterManager", USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance)
return {"extension":USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance(), "output_device": USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance()}

View file

View file

@ -0,0 +1,25 @@
"""
Database of AVR chips for avr_isp programming. Contains signatures and flash sizes from the AVR datasheets.
To support more chips add the relevant data to the avrChipDB list.
This is a python 3 conversion of the code created by David Braam for the Cura project.
"""
avr_chip_db = {
"ATMega1280": {
"signature": [0x1E, 0x97, 0x03],
"pageSize": 128,
"pageCount": 512,
},
"ATMega2560": {
"signature": [0x1E, 0x98, 0x01],
"pageSize": 128,
"pageCount": 1024,
},
}
def getChipFromDB(sig):
for chip in avr_chip_db.values():
if chip["signature"] == sig:
return chip
return False

View file

@ -0,0 +1,46 @@
"""
Module to read intel hex files into binary data blobs.
IntelHex files are commonly used to distribute firmware
See: http://en.wikipedia.org/wiki/Intel_HEX
This is a python 3 conversion of the code created by David Braam for the Cura project.
"""
import io
def readHex(filename):
"""
Read an verify an intel hex file. Return the data as an list of bytes.
"""
data = []
extra_addr = 0
f = io.open(filename, "r")
for line in f:
line = line.strip()
if len(line) < 1:
continue
if line[0] != ":":
raise Exception("Hex file has a line not starting with ':'")
rec_len = int(line[1:3], 16)
addr = int(line[3:7], 16) + extra_addr
rec_type = int(line[7:9], 16)
if len(line) != rec_len * 2 + 11:
raise Exception("Error in hex file: " + line)
check_sum = 0
for i in range(0, rec_len + 5):
check_sum += int(line[i*2+1:i*2+3], 16)
check_sum &= 0xFF
if check_sum != 0:
raise Exception("Checksum error in hex file: " + line)
if rec_type == 0:#Data record
while len(data) < addr + rec_len:
data.append(0)
for i in range(0, rec_len):
data[addr + i] = int(line[i*2+9:i*2+11], 16)
elif rec_type == 1: #End Of File record
pass
elif rec_type == 2: #Extended Segment Address Record
extra_addr = int(line[9:13], 16) * 16
else:
print(rec_type, rec_len, addr, check_sum, line)
f.close()
return data

View file

@ -0,0 +1,66 @@
"""
General interface for Isp based AVR programmers.
The ISP AVR programmer can load firmware into AVR chips. Which are commonly used on 3D printers.
Needs to be subclassed to support different programmers.
Currently only the stk500v2 subclass exists.
This is a python 3 conversion of the code created by David Braam for the Cura project.
"""
from . import chipDB
class IspBase():
"""
Base class for ISP based AVR programmers.
Functions in this class raise an IspError when something goes wrong.
"""
def programChip(self, flash_data):
""" Program a chip with the given flash data. """
self.cur_ext_addr = -1
self.chip = chipDB.getChipFromDB(self.getSignature())
if not self.chip:
raise IspError("Chip with signature: " + str(self.getSignature()) + "not found")
self.chipErase()
print("Flashing %i bytes" % len(flash_data))
self.writeFlash(flash_data)
print("Verifying %i bytes" % len(flash_data))
self.verifyFlash(flash_data)
print("Completed")
def getSignature(self):
"""
Get the AVR signature from the chip. This is a 3 byte array which describes which chip we are connected to.
This is important to verify that we are programming the correct type of chip and that we use proper flash block sizes.
"""
sig = []
sig.append(self.sendISP([0x30, 0x00, 0x00, 0x00])[3])
sig.append(self.sendISP([0x30, 0x00, 0x01, 0x00])[3])
sig.append(self.sendISP([0x30, 0x00, 0x02, 0x00])[3])
return sig
def chipErase(self):
"""
Do a full chip erase, clears all data, and lockbits.
"""
self.sendISP([0xAC, 0x80, 0x00, 0x00])
def writeFlash(self, flash_data):
"""
Write the flash data, needs to be implemented in a subclass.
"""
raise IspError("Called undefined writeFlash")
def verifyFlash(self, flash_data):
"""
Verify the flash data, needs to be implemented in a subclass.
"""
raise IspError("Called undefined verifyFlash")
class IspError(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)

View file

@ -0,0 +1,220 @@
"""
STK500v2 protocol implementation for programming AVR chips.
The STK500v2 protocol is used by the ArduinoMega2560 and a few other Arduino platforms to load firmware.
This is a python 3 conversion of the code created by David Braam for the Cura project.
"""
import os
import struct
import sys
import time
from serial import Serial
from serial import SerialException
from serial import SerialTimeoutException
from . import ispBase, intelHex
class Stk500v2(ispBase.IspBase):
def __init__(self):
self.serial = None
self.seq = 1
self.last_addr = -1
self.progress_callback = None
def connect(self, port = "COM22", speed = 115200):
if self.serial is not None:
self.close()
try:
self.serial = Serial(str(port), speed, timeout=1, writeTimeout=10000)
except SerialException as e:
raise ispBase.IspError("Failed to open serial port")
except:
raise ispBase.IspError("Unexpected error while connecting to serial port:" + port + ":" + str(sys.exc_info()[0]))
self.seq = 1
#Reset the controller
for n in range(0, 2):
self.serial.setDTR(True)
time.sleep(0.1)
self.serial.setDTR(False)
time.sleep(0.1)
time.sleep(0.2)
self.serial.flushInput()
self.serial.flushOutput()
if self.sendMessage([0x10, 0xc8, 0x64, 0x19, 0x20, 0x00, 0x53, 0x03, 0xac, 0x53, 0x00, 0x00]) != [0x10, 0x00]:
self.close()
raise ispBase.IspError("Failed to enter programming mode")
self.sendMessage([0x06, 0x80, 0x00, 0x00, 0x00])
if self.sendMessage([0xEE])[1] == 0x00:
self._has_checksum = True
else:
self._has_checksum = False
self.serial.timeout = 5
def close(self):
if self.serial is not None:
self.serial.close()
self.serial = None
#Leave ISP does not reset the serial port, only resets the device, and returns the serial port after disconnecting it from the programming interface.
# This allows you to use the serial port without opening it again.
def leaveISP(self):
if self.serial is not None:
if self.sendMessage([0x11]) != [0x11, 0x00]:
raise ispBase.IspError("Failed to leave programming mode")
ret = self.serial
self.serial = None
return ret
return None
def isConnected(self):
return self.serial is not None
def hasChecksumFunction(self):
return self._has_checksum
def sendISP(self, data):
recv = self.sendMessage([0x1D, 4, 4, 0, data[0], data[1], data[2], data[3]])
return recv[2:6]
def writeFlash(self, flash_data):
#Set load addr to 0, in case we have more then 64k flash we need to enable the address extension
page_size = self.chip["pageSize"] * 2
flash_size = page_size * self.chip["pageCount"]
print("Writing flash")
if flash_size > 0xFFFF:
self.sendMessage([0x06, 0x80, 0x00, 0x00, 0x00])
else:
self.sendMessage([0x06, 0x00, 0x00, 0x00, 0x00])
load_count = (len(flash_data) + page_size - 1) / page_size
for i in range(0, int(load_count)):
recv = self.sendMessage([0x13, page_size >> 8, page_size & 0xFF, 0xc1, 0x0a, 0x40, 0x4c, 0x20, 0x00, 0x00] + flash_data[(i * page_size):(i * page_size + page_size)])
if self.progress_callback is not None:
if self._has_checksum:
self.progress_callback(i + 1, load_count)
else:
self.progress_callback(i + 1, load_count * 2)
def verifyFlash(self, flash_data):
if self._has_checksum:
self.sendMessage([0x06, 0x00, (len(flash_data) >> 17) & 0xFF, (len(flash_data) >> 9) & 0xFF, (len(flash_data) >> 1) & 0xFF])
res = self.sendMessage([0xEE])
checksum_recv = res[2] | (res[3] << 8)
checksum = 0
for d in flash_data:
checksum += d
checksum &= 0xFFFF
if hex(checksum) != hex(checksum_recv):
raise ispBase.IspError("Verify checksum mismatch: 0x%x != 0x%x" % (checksum & 0xFFFF, checksum_recv))
else:
#Set load addr to 0, in case we have more then 64k flash we need to enable the address extension
flash_size = self.chip["pageSize"] * 2 * self.chip["pageCount"]
if flash_size > 0xFFFF:
self.sendMessage([0x06, 0x80, 0x00, 0x00, 0x00])
else:
self.sendMessage([0x06, 0x00, 0x00, 0x00, 0x00])
load_count = (len(flash_data) + 0xFF) / 0x100
for i in range(0, int(load_count)):
recv = self.sendMessage([0x14, 0x01, 0x00, 0x20])[2:0x102]
if self.progress_callback is not None:
self.progress_callback(load_count + i + 1, load_count * 2)
for j in range(0, 0x100):
if i * 0x100 + j < len(flash_data) and flash_data[i * 0x100 + j] != recv[j]:
raise ispBase.IspError("Verify error at: 0x%x" % (i * 0x100 + j))
def sendMessage(self, data):
message = struct.pack(">BBHB", 0x1B, self.seq, len(data), 0x0E)
for c in data:
message += struct.pack(">B", c)
checksum = 0
for c in message:
checksum ^= c
message += struct.pack(">B", checksum)
try:
self.serial.write(message)
self.serial.flush()
except SerialTimeoutException:
raise ispBase.IspError("Serial send timeout")
self.seq = (self.seq + 1) & 0xFF
return self.recvMessage()
def recvMessage(self):
state = "Start"
checksum = 0
while True:
s = self.serial.read()
if len(s) < 1:
raise ispBase.IspError("Timeout")
b = struct.unpack(">B", s)[0]
checksum ^= b
#print(hex(b))
if state == "Start":
if b == 0x1B:
state = "GetSeq"
checksum = 0x1B
elif state == "GetSeq":
state = "MsgSize1"
elif state == "MsgSize1":
msg_size = b << 8
state = "MsgSize2"
elif state == "MsgSize2":
msg_size |= b
state = "Token"
elif state == "Token":
if b != 0x0E:
state = "Start"
else:
state = "Data"
data = []
elif state == "Data":
data.append(b)
if len(data) == msg_size:
state = "Checksum"
elif state == "Checksum":
if checksum != 0:
state = "Start"
else:
return data
def portList():
ret = []
import _winreg
key=_winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE,"HARDWARE\\DEVICEMAP\\SERIALCOMM")
i=0
while True:
try:
values = _winreg.EnumValue(key, i)
except:
return ret
if "USBSER" in values[0]:
ret.append(values[1])
i+=1
return ret
def runProgrammer(port, filename):
""" Run an STK500v2 program on serial port 'port' and write 'filename' into flash. """
programmer = Stk500v2()
programmer.connect(port = port)
programmer.programChip(intelHex.readHex(filename))
programmer.close()
def main():
""" Entry point to call the stk500v2 programmer from the commandline. """
import threading
if sys.argv[1] == "AUTO":
print(portList())
for port in portList():
threading.Thread(target=runProgrammer, args=(port,sys.argv[2])).start()
time.sleep(5)
else:
programmer = Stk500v2()
programmer.connect(port = sys.argv[1])
programmer.programChip(intelHex.readHex(sys.argv[2]))
sys.exit(1)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,61 @@
from cura.MachineAction import MachineAction
from cura.PrinterOutputDevice import PrinterOutputDevice
from PyQt5.QtCore import pyqtSlot
from UM.Application import Application
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class BedLevelMachineAction(MachineAction):
def __init__(self):
super().__init__("BedLevel", catalog.i18nc("@action", "Level build plate"))
self._qml_url = "BedLevelMachineAction.qml"
self._bed_level_position = 0
def _execute(self):
pass
def _reset(self):
self._bed_level_position = 0
pass
@pyqtSlot()
def startBedLeveling(self):
self._bed_level_position = 0
printer_output_devices = self._getPrinterOutputDevices()
if printer_output_devices:
printer_output_devices[0].homeBed()
printer_output_devices[0].moveHead(0, 0, 3)
printer_output_devices[0].homeHead()
def _getPrinterOutputDevices(self):
return [printer_output_device for printer_output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices() if isinstance(printer_output_device, PrinterOutputDevice)]
@pyqtSlot()
def moveToNextLevelPosition(self):
output_devices = self._getPrinterOutputDevices()
if output_devices: # We found at least one output device
output_device = output_devices[0]
if self._bed_level_position == 0:
output_device.moveHead(0, 0, 3)
output_device.homeHead()
output_device.moveHead(0, 0, 3)
output_device.moveHead(Application.getInstance().getGlobalContainerStack().getProperty("machine_width", "value") - 10, 0, 0)
output_device.moveHead(0, 0, -3)
self._bed_level_position += 1
elif self._bed_level_position == 1:
output_device.moveHead(0, 0, 3)
output_device.moveHead(-Application.getInstance().getGlobalContainerStack().getProperty("machine_width", "value" ) / 2, Application.getInstance().getGlobalContainerStack().getProperty("machine_depth", "value") - 10, 0)
output_device.moveHead(0, 0, -3)
self._bed_level_position += 1
elif self._bed_level_position == 2:
output_device.moveHead(0, 0, 3)
output_device.moveHead(-Application.getInstance().getGlobalContainerStack().getProperty("machine_width", "value") / 2 + 10, -(Application.getInstance().getGlobalContainerStack().getProperty("machine_depth", "value") + 10), 0)
output_device.moveHead(0, 0, -3)
self._bed_level_position += 1
elif self._bed_level_position >= 3:
output_device.sendCommand("M18") # Turn off all motors so the user can move the axes
self.setFinished()

View file

@ -0,0 +1,85 @@
// Copyright (c) 2016 Ultimaker B.V.
// Cura is released under the terms of the AGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
import UM 1.2 as UM
import Cura 1.0 as Cura
Cura.MachineAction
{
anchors.fill: parent;
Item
{
id: bedLevelMachineAction
anchors.fill: parent;
UM.I18nCatalog { id: catalog; name: "cura"; }
Label
{
id: pageTitle
width: parent.width
text: catalog.i18nc("@title", "Build Plate Leveling")
wrapMode: Text.WordWrap
font.pointSize: 18;
}
Label
{
id: pageDescription
anchors.top: pageTitle.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "To make sure your prints will come out great, you can now adjust your buildplate. When you click 'Move to Next Position' the nozzle will move to the different positions that can be adjusted.")
}
Label
{
id: bedlevelingText
anchors.top: pageDescription.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "For every position; insert a piece of paper under the nozzle and adjust the print build plate height. The print build plate height is right when the paper is slightly gripped by the tip of the nozzle.")
}
Row
{
id: bedlevelingWrapper
anchors.top: bedlevelingText.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.horizontalCenter: parent.horizontalCenter
width: childrenRect.width
spacing: UM.Theme.getSize("default_margin").width
Button
{
id: startBedLevelingButton
text: catalog.i18nc("@action:button","Start Build Plate Leveling")
onClicked:
{
startBedLevelingButton.visible = false;
bedlevelingButton.visible = true;
checkupMachineAction.heatupHotendStarted = false;
checkupMachineAction.heatupBedStarted = false;
manager.startCheck();
}
}
Button
{
id: bedlevelingButton
text: catalog.i18nc("@action:button","Move to Next Position")
visible: false
onClicked:
{
manager.moveToNextLevelPosition();
}
}
}
}
}

View file

@ -0,0 +1,193 @@
from cura.MachineAction import MachineAction
from cura.PrinterOutputDevice import PrinterOutputDevice
from UM.Application import Application
from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty
from UM.Logger import Logger
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class UMOCheckupMachineAction(MachineAction):
def __init__(self):
super().__init__("UMOCheckup", catalog.i18nc("@action", "Checkup"))
self._qml_url = "UMOCheckupMachineAction.qml"
self._hotend_target_temp = 180
self._bed_target_temp = 60
self._output_device = None
self._bed_test_completed = False
self._hotend_test_completed = False
# Endstop tests
self._x_min_endstop_test_completed = False
self._y_min_endstop_test_completed = False
self._z_min_endstop_test_completed = False
self._check_started = False
Application.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
onBedTestCompleted = pyqtSignal()
onHotendTestCompleted = pyqtSignal()
onXMinEndstopTestCompleted = pyqtSignal()
onYMinEndstopTestCompleted = pyqtSignal()
onZMinEndstopTestCompleted = pyqtSignal()
bedTemperatureChanged = pyqtSignal()
hotendTemperatureChanged = pyqtSignal()
def _onOutputDevicesChanged(self):
# Check if this action was started, but no output device was found the first time.
# If so, re-try now that an output device has been added/removed.
if self._output_device is None and self._check_started:
self.startCheck()
def _getPrinterOutputDevices(self):
return [printer_output_device for printer_output_device in
Application.getInstance().getOutputDeviceManager().getOutputDevices() if
isinstance(printer_output_device, PrinterOutputDevice)]
def _reset(self):
if self._output_device:
self._output_device.bedTemperatureChanged.disconnect(self.bedTemperatureChanged)
self._output_device.hotendTemperaturesChanged.disconnect(self.hotendTemperatureChanged)
self._output_device.bedTemperatureChanged.disconnect(self._onBedTemperatureChanged)
self._output_device.hotendTemperaturesChanged.disconnect(self._onHotendTemperatureChanged)
self._output_device.endstopStateChanged.disconnect(self._onEndstopStateChanged)
try:
self._output_device.stopPollEndstop()
except AttributeError as e: # Connection is probably not a USB connection. Something went pretty wrong if this happens.
Logger.log("e", "An exception occurred while stopping end stop polling: %s" % str(e))
self._output_device = None
self._check_started = False
self.checkStartedChanged.emit()
# Ensure everything is reset (and right signals are emitted again)
self._bed_test_completed = False
self.onBedTestCompleted.emit()
self._hotend_test_completed = False
self.onHotendTestCompleted.emit()
self._x_min_endstop_test_completed = False
self.onXMinEndstopTestCompleted.emit()
self._y_min_endstop_test_completed = False
self.onYMinEndstopTestCompleted.emit()
self._z_min_endstop_test_completed = False
self.onZMinEndstopTestCompleted.emit()
self.heatedBedChanged.emit()
@pyqtProperty(bool, notify = onBedTestCompleted)
def bedTestCompleted(self):
return self._bed_test_completed
@pyqtProperty(bool, notify = onHotendTestCompleted)
def hotendTestCompleted(self):
return self._hotend_test_completed
@pyqtProperty(bool, notify = onXMinEndstopTestCompleted)
def xMinEndstopTestCompleted(self):
return self._x_min_endstop_test_completed
@pyqtProperty(bool, notify=onYMinEndstopTestCompleted)
def yMinEndstopTestCompleted(self):
return self._y_min_endstop_test_completed
@pyqtProperty(bool, notify=onZMinEndstopTestCompleted)
def zMinEndstopTestCompleted(self):
return self._z_min_endstop_test_completed
@pyqtProperty(float, notify = bedTemperatureChanged)
def bedTemperature(self):
if not self._output_device:
return 0
return self._output_device.bedTemperature
@pyqtProperty(float, notify=hotendTemperatureChanged)
def hotendTemperature(self):
if not self._output_device:
return 0
return self._output_device.hotendTemperatures[0]
def _onHotendTemperatureChanged(self):
if not self._output_device:
return
if not self._hotend_test_completed:
if self._output_device.hotendTemperatures[0] + 10 > self._hotend_target_temp and self._output_device.hotendTemperatures[0] - 10 < self._hotend_target_temp:
self._hotend_test_completed = True
self.onHotendTestCompleted.emit()
def _onBedTemperatureChanged(self):
if not self._output_device:
return
if not self._bed_test_completed:
if self._output_device.bedTemperature + 5 > self._bed_target_temp and self._output_device.bedTemperature - 5 < self._bed_target_temp:
self._bed_test_completed = True
self.onBedTestCompleted.emit()
def _onEndstopStateChanged(self, switch_type, state):
if state:
if switch_type == "x_min":
self._x_min_endstop_test_completed = True
self.onXMinEndstopTestCompleted.emit()
elif switch_type == "y_min":
self._y_min_endstop_test_completed = True
self.onYMinEndstopTestCompleted.emit()
elif switch_type == "z_min":
self._z_min_endstop_test_completed = True
self.onZMinEndstopTestCompleted.emit()
checkStartedChanged = pyqtSignal()
@pyqtProperty(bool, notify = checkStartedChanged)
def checkStarted(self):
return self._check_started
@pyqtSlot()
def startCheck(self):
self._check_started = True
self.checkStartedChanged.emit()
output_devices = self._getPrinterOutputDevices()
if output_devices:
self._output_device = output_devices[0]
try:
self._output_device.sendCommand("M18") # Turn off all motors so the user can move the axes
self._output_device.startPollEndstop()
self._output_device.bedTemperatureChanged.connect(self.bedTemperatureChanged)
self._output_device.hotendTemperaturesChanged.connect(self.hotendTemperatureChanged)
self._output_device.bedTemperatureChanged.connect(self._onBedTemperatureChanged)
self._output_device.hotendTemperaturesChanged.connect(self._onHotendTemperatureChanged)
self._output_device.endstopStateChanged.connect(self._onEndstopStateChanged)
except AttributeError as e: # Connection is probably not a USB connection. Something went pretty wrong if this happens.
Logger.log("e", "An exception occurred while starting end stop polling: %s" % str(e))
@pyqtSlot()
def cooldownHotend(self):
if self._output_device is not None:
self._output_device.setTargetHotendTemperature(0, 0)
@pyqtSlot()
def cooldownBed(self):
if self._output_device is not None:
self._output_device.setTargetBedTemperature(0)
@pyqtSlot()
def heatupHotend(self):
if self._output_device is not None:
self._output_device.setTargetHotendTemperature(0, self._hotend_target_temp)
@pyqtSlot()
def heatupBed(self):
if self._output_device is not None:
self._output_device.setTargetBedTemperature(self._bed_target_temp)
heatedBedChanged = pyqtSignal()
@pyqtProperty(bool, notify = heatedBedChanged)
def hasHeatedBed(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
return global_container_stack.getProperty("machine_heated_bed", "value")

View file

@ -0,0 +1,288 @@
import UM 1.2 as UM
import Cura 1.0 as Cura
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
Cura.MachineAction
{
anchors.fill: parent;
Item
{
id: checkupMachineAction
anchors.fill: parent;
property int leftRow: checkupMachineAction.width * 0.40
property int rightRow: checkupMachineAction.width * 0.60
property bool heatupHotendStarted: false
property bool heatupBedStarted: false
property bool usbConnected: Cura.USBPrinterManager.connectedPrinterList.rowCount() > 0
UM.I18nCatalog { id: catalog; name:"cura"}
Label
{
id: pageTitle
width: parent.width
text: catalog.i18nc("@title", "Check Printer")
wrapMode: Text.WordWrap
font.pointSize: 18;
}
Label
{
id: pageDescription
anchors.top: pageTitle.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "It's a good idea to do a few sanity checks on your Ultimaker. You can skip this step if you know your machine is functional");
}
Row
{
id: startStopButtons
anchors.top: pageDescription.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.horizontalCenter: parent.horizontalCenter
width: childrenRect.width
spacing: UM.Theme.getSize("default_margin").width
Button
{
id: startCheckButton
text: catalog.i18nc("@action:button","Start Printer Check");
onClicked:
{
checkupMachineAction.heatupHotendStarted = false;
checkupMachineAction.heatupBedStarted = false;
manager.startCheck();
startCheckButton.visible = false;
}
}
}
Item
{
id: checkupContent
anchors.top: startStopButtons.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
visible: manager.checkStarted
width: parent.width
height: 250
//////////////////////////////////////////////////////////
Label
{
id: connectionLabel
width: checkupMachineAction.leftRow
anchors.left: parent.left
anchors.top: parent.top
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Connection: ")
}
Label
{
id: connectionStatus
width: checkupMachineAction.rightRow
anchors.left: connectionLabel.right
anchors.top: parent.top
wrapMode: Text.WordWrap
text: checkupMachineAction.usbConnected ? catalog.i18nc("@info:status","Connected"): catalog.i18nc("@info:status","Not connected")
}
//////////////////////////////////////////////////////////
Label
{
id: endstopXLabel
width: checkupMachineAction.leftRow
anchors.left: parent.left
anchors.top: connectionLabel.bottom
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Min endstop X: ")
visible: checkupMachineAction.usbConnected
}
Label
{
id: endstopXStatus
width: checkupMachineAction.rightRow
anchors.left: endstopXLabel.right
anchors.top: connectionLabel.bottom
wrapMode: Text.WordWrap
text: manager.xMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked")
visible: checkupMachineAction.usbConnected
}
//////////////////////////////////////////////////////////////
Label
{
id: endstopYLabel
width: checkupMachineAction.leftRow
anchors.left: parent.left
anchors.top: endstopXLabel.bottom
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Min endstop Y: ")
visible: checkupMachineAction.usbConnected
}
Label
{
id: endstopYStatus
width: checkupMachineAction.rightRow
anchors.left: endstopYLabel.right
anchors.top: endstopXLabel.bottom
wrapMode: Text.WordWrap
text: manager.yMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked")
visible: checkupMachineAction.usbConnected
}
/////////////////////////////////////////////////////////////////////
Label
{
id: endstopZLabel
width: checkupMachineAction.leftRow
anchors.left: parent.left
anchors.top: endstopYLabel.bottom
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Min endstop Z: ")
visible: checkupMachineAction.usbConnected
}
Label
{
id: endstopZStatus
width: checkupMachineAction.rightRow
anchors.left: endstopZLabel.right
anchors.top: endstopYLabel.bottom
wrapMode: Text.WordWrap
text: manager.zMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked")
visible: checkupMachineAction.usbConnected
}
////////////////////////////////////////////////////////////
Label
{
id: nozzleTempLabel
width: checkupMachineAction.leftRow
height: nozzleTempButton.height
anchors.left: parent.left
anchors.top: endstopZLabel.bottom
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Nozzle temperature check: ")
visible: checkupMachineAction.usbConnected
}
Label
{
id: nozzleTempStatus
width: checkupMachineAction.rightRow * 0.4
anchors.top: nozzleTempLabel.top
anchors.left: nozzleTempLabel.right
wrapMode: Text.WordWrap
text: catalog.i18nc("@info:status","Not checked")
visible: checkupMachineAction.usbConnected
}
Item
{
id: nozzleTempButton
width: checkupMachineAction.rightRow * 0.3
height: childrenRect.height
anchors.top: nozzleTempLabel.top
anchors.left: bedTempStatus.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width/2
visible: checkupMachineAction.usbConnected
Button
{
text: checkupMachineAction.heatupHotendStarted ? catalog.i18nc("@action:button","Stop Heating") : catalog.i18nc("@action:button","Start Heating")
onClicked:
{
if (checkupMachineAction.heatupHotendStarted)
{
manager.cooldownHotend()
checkupMachineAction.heatupHotendStarted = false
} else
{
manager.heatupHotend()
checkupMachineAction.heatupHotendStarted = true
}
}
}
}
Label
{
id: nozzleTemp
anchors.top: nozzleTempLabel.top
anchors.left: nozzleTempButton.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
width: checkupMachineAction.rightRow * 0.2
wrapMode: Text.WordWrap
text: manager.hotendTemperature + "°C"
font.bold: true
visible: checkupMachineAction.usbConnected
}
/////////////////////////////////////////////////////////////////////////////
Label
{
id: bedTempLabel
width: checkupMachineAction.leftRow
height: bedTempButton.height
anchors.left: parent.left
anchors.top: nozzleTempLabel.bottom
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Build plate temperature check:")
visible: checkupMachineAction.usbConnected && manager.hasHeatedBed
}
Label
{
id: bedTempStatus
width: checkupMachineAction.rightRow * 0.4
anchors.top: bedTempLabel.top
anchors.left: bedTempLabel.right
wrapMode: Text.WordWrap
text: manager.bedTestCompleted ? catalog.i18nc("@info:status","Not checked"): catalog.i18nc("@info:status","Checked")
visible: checkupMachineAction.usbConnected && manager.hasHeatedBed
}
Item
{
id: bedTempButton
width: checkupMachineAction.rightRow * 0.3
height: childrenRect.height
anchors.top: bedTempLabel.top
anchors.left: bedTempStatus.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width/2
visible: checkupMachineAction.usbConnected && manager.hasHeatedBed
Button
{
text: checkupMachineAction.heatupBedStarted ?catalog.i18nc("@action:button","Stop Heating") : catalog.i18nc("@action:button","Start Heating")
onClicked:
{
if (checkupMachineAction.heatupBedStarted)
{
manager.cooldownBed()
checkupMachineAction.heatupBedStarted = false
} else
{
manager.heatupBed()
checkupMachineAction.heatupBedStarted = true
}
}
}
}
Label
{
id: bedTemp
width: checkupMachineAction.rightRow * 0.2
anchors.top: bedTempLabel.top
anchors.left: bedTempButton.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
wrapMode: Text.WordWrap
text: manager.bedTemperature + "°C"
font.bold: true
visible: checkupMachineAction.usbConnected && manager.hasHeatedBed
}
Label
{
id: resultText
visible: false
anchors.top: bedTemp.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.left: parent.left
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Everything is in order! You're done with your CheckUp.")
}
}
}
}

View file

@ -0,0 +1,50 @@
from cura.MachineAction import MachineAction
from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty
from UM.i18n import i18nCatalog
from UM.Application import Application
catalog = i18nCatalog("cura")
import UM.Settings.InstanceContainer
class UMOUpgradeSelection(MachineAction):
def __init__(self):
super().__init__("UMOUpgradeSelection", catalog.i18nc("@action", "Select upgrades"))
self._qml_url = "UMOUpgradeSelectionMachineAction.qml"
def _reset(self):
self.heatedBedChanged.emit()
heatedBedChanged = pyqtSignal()
@pyqtProperty(bool, notify = heatedBedChanged)
def hasHeatedBed(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
return global_container_stack.getProperty("machine_heated_bed", "value")
@pyqtSlot()
def addHeatedBed(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
variant = global_container_stack.findContainer({"type": "variant"})
if variant:
if variant.getId() == "empty_variant":
variant_index = global_container_stack.getContainerIndex(variant)
stack_name = global_container_stack.getName()
new_variant = UM.Settings.InstanceContainer(stack_name + "_variant")
new_variant.addMetaDataEntry("type", "variant")
new_variant.setDefinition(global_container_stack.getBottom())
UM.Settings.ContainerRegistry.getInstance().addContainer(new_variant)
global_container_stack.replaceContainer(variant_index, new_variant)
variant = new_variant
variant.setProperty("machine_heated_bed", "value", True)
self.heatedBedChanged.emit()
@pyqtSlot()
def removeHeatedBed(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
variant = global_container_stack.findContainer({"type": "variant"})
if variant:
variant.setProperty("machine_heated_bed", "value", False)
self.heatedBedChanged.emit()

View file

@ -0,0 +1,52 @@
// Copyright (c) 2016 Ultimaker B.V.
// Cura is released under the terms of the AGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
import UM 1.2 as UM
import Cura 1.0 as Cura
Cura.MachineAction
{
anchors.fill: parent;
Item
{
id: upgradeSelectionMachineAction
anchors.fill: parent
Label
{
id: pageTitle
width: parent.width
text: catalog.i18nc("@title", "Select Printer Upgrades")
wrapMode: Text.WordWrap
font.pointSize: 18;
}
Label
{
id: pageDescription
anchors.top: pageTitle.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Please select any upgrades made to this Ultimaker Original");
}
CheckBox
{
anchors.top: pageDescription.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
text: catalog.i18nc("@label", "Heated Build Plate (official kit or self-built)")
checked: manager.hasHeatedBed
onClicked: checked ? manager.addHeatedBed() : manager.removeHeatedBed()
}
UM.I18nCatalog { id: catalog; name: "cura"; }
}
}

View file

@ -0,0 +1,9 @@
from cura.MachineAction import MachineAction
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class UpgradeFirmwareMachineAction(MachineAction):
def __init__(self):
super().__init__("UpgradeFirmware", catalog.i18nc("@action", "Upgrade Firmware"))
self._qml_url = "UpgradeFirmwareMachineAction.qml"

View file

@ -0,0 +1,77 @@
// Copyright (c) 2016 Ultimaker B.V.
// Cura is released under the terms of the AGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
import UM 1.2 as UM
import Cura 1.0 as Cura
Cura.MachineAction
{
anchors.fill: parent;
Item
{
id: upgradeFirmwareMachineAction
anchors.fill: parent;
UM.I18nCatalog { id: catalog; name:"cura"}
Label
{
id: pageTitle
width: parent.width
text: catalog.i18nc("@title", "Upgrade Firmware")
wrapMode: Text.WordWrap
font.pointSize: 18
}
Label
{
id: pageDescription
anchors.top: pageTitle.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Firmware is the piece of software running directly on your 3D printer. This firmware controls the step motors, regulates the temperature and ultimately makes your printer work.")
}
Label
{
id: upgradeText1
anchors.top: pageDescription.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "The firmware shipping with new Ultimakers works, but upgrades have been made to make better prints, and make calibration easier.");
}
Label
{
id: upgradeText2
anchors.top: upgradeText1.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Cura requires these new features and thus your firmware will most likely need to be upgraded. You can do so now.");
}
Row
{
anchors.top: upgradeText2.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.horizontalCenter: parent.horizontalCenter
width: childrenRect.width
spacing: UM.Theme.getSize("default_margin").width
Button
{
id: upgradeButton
text: catalog.i18nc("@action:button","Upgrade to Marlin Firmware");
onClicked:
{
Cura.USBPrinterManager.updateAllFirmware()
}
}
}
}
}

View file

@ -0,0 +1,24 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from . import BedLevelMachineAction
from . import UpgradeFirmwareMachineAction
from . import UMOCheckupMachineAction
from . import UMOUpgradeSelection
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {
"plugin": {
"name": catalog.i18nc("@label", "Ultimaker machine actions"),
"author": "Ultimaker",
"version": "1.0",
"description": catalog.i18nc("@info:whatsthis", "Provides machine actions for Ultimaker machines (such as bed leveling wizard, selecting upgrades, etc)"),
"api": 3
}
}
def register(app):
return { "machine_action": [BedLevelMachineAction.BedLevelMachineAction(), UpgradeFirmwareMachineAction.UpgradeFirmwareMachineAction(), UMOCheckupMachineAction.UMOCheckupMachineAction(), UMOUpgradeSelection.UMOUpgradeSelection()]}

View file

@ -0,0 +1,108 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
import UM.VersionUpgrade #To indicate that a file is of incorrect format.
import configparser #To read config files.
import io #To write config files to strings as if they were files.
## Creates a new machine instance instance by parsing a serialised machine
# instance in version 1 of the file format.
#
# \param serialised The serialised form of a machine instance in version 1.
# \param filename The supposed file name of this machine instance, without
# extension.
# \return A machine instance instance, or None if the file format is
# incorrect.
def importFrom(serialised, filename):
try:
return MachineInstance(serialised, filename)
except (configparser.Error, UM.VersionUpgrade.FormatException, UM.VersionUpgrade.InvalidVersionException):
return None
## A representation of a machine instance used as intermediary form for
# conversion from one format to the other.
class MachineInstance:
## Reads version 1 of the file format, storing it in memory.
#
# \param serialised A string with the contents of a machine instance file,
# without extension.
# \param filename The supposed file name of this machine instance.
def __init__(self, serialised, filename):
self._filename = filename
config = configparser.ConfigParser(interpolation = None)
config.read_string(serialised) # Read the input string as config file.
# Checking file correctness.
if not config.has_section("general"):
raise UM.VersionUpgrade.FormatException("No \"general\" section.")
if not config.has_option("general", "version"):
raise UM.VersionUpgrade.FormatException("No \"version\" in \"general\" section.")
if not config.has_option("general", "name"):
raise UM.VersionUpgrade.FormatException("No \"name\" in \"general\" section.")
if not config.has_option("general", "type"):
raise UM.VersionUpgrade.FormatException("No \"type\" in \"general\" section.")
if int(config.get("general", "version")) != 1: # Explicitly hard-code version 1, since if this number changes the programmer MUST change this entire function.
raise UM.VersionUpgrade.InvalidVersionException("The version of this machine instance is wrong. It must be 1.")
self._type_name = config.get("general", "type")
self._variant_name = config.get("general", "variant", fallback = "empty_variant")
self._name = config.get("general", "name", fallback = "")
self._key = config.get("general", "key", fallback = None)
self._active_profile_name = config.get("general", "active_profile", fallback = "empty_quality")
self._active_material_name = config.get("general", "material", fallback = "empty_material")
self._machine_setting_overrides = {}
for key, value in config["machine_settings"].items():
self._machine_setting_overrides[key] = value
## Serialises this machine instance as file format version 2.
#
# This is where the actual translation happens in this case.
#
# \return A tuple containing the new filename and a serialised form of
# this machine instance, serialised in version 2 of the file format.
def export(self):
config = configparser.ConfigParser(interpolation = None) # Build a config file in the form of version 2.
config.add_section("general")
config.set("general", "name", self._name)
config.set("general", "id", self._name)
config.set("general", "type", self._type_name)
config.set("general", "version", "2") # Hard-code version 2, since if this number changes the programmer MUST change this entire function.
import VersionUpgrade21to22 # Import here to prevent circular dependencies.
has_machine_qualities = self._type_name in VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.machinesWithMachineQuality()
type_name = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translatePrinter(self._type_name)
active_material = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateProfile(self._active_material_name)
variant = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateVariant(self._variant_name, type_name)
variant_materials = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateVariantForMaterials(self._variant_name, type_name)
active_profile = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateProfile(self._active_profile_name)
if has_machine_qualities: #This machine now has machine-quality profiles.
active_profile += "_" + active_material + "_" + variant
active_material += "_" + variant_materials #That means that the profile was split into multiple.
current_settings = "empty" #The profile didn't know the definition ID when it was upgraded, so it will have been invalid. Sorry, your current settings are lost now.
else:
current_settings = self._name + "_current_settings"
containers = [
current_settings,
active_profile,
active_material,
variant,
type_name
]
config.set("general", "containers", ",".join(containers))
config.add_section("metadata")
config.set("metadata", "type", "machine")
VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateSettings(self._machine_setting_overrides)
config.add_section("values")
for key, value in self._machine_setting_overrides.items():
config.set("values", key, str(value))
output = io.StringIO()
config.write(output)
return [self._filename], [output.getvalue()]

View file

@ -0,0 +1,80 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
import configparser #To read config files.
import io #To output config files to string.
import UM.VersionUpgrade #To indicate that a file is of the wrong format.
## Creates a new preferences instance by parsing a serialised preferences file
# in version 1 of the file format.
#
# \param serialised The serialised form of a preferences file in version 1.
# \param filename The supposed filename of the preferences file, without
# extension.
# \return A representation of those preferences, or None if the file format is
# incorrect.
def importFrom(serialised, filename):
try:
return Preferences(serialised, filename)
except (configparser.Error, UM.VersionUpgrade.FormatException, UM.VersionUpgrade.InvalidVersionException):
return None
## A representation of preferences files as intermediary form for conversion
# from one format to the other.
class Preferences:
## Reads version 2 of the preferences file format, storing it in memory.
#
# \param serialised A serialised version 2 preferences file.
# \param filename The supposed filename of the preferences file, without
# extension.
def __init__(self, serialised, filename):
self._filename = filename
self._config = configparser.ConfigParser(interpolation = None)
self._config.read_string(serialised)
#Checking file correctness.
if not self._config.has_section("general"):
raise UM.VersionUpgrade.FormatException("No \"general\" section.")
if not self._config.has_option("general", "version"):
raise UM.VersionUpgrade.FormatException("No \"version\" in \"general\" section.")
if int(self._config.get("general", "version")) != 2: # Explicitly hard-code version 2, since if this number changes the programmer MUST change this entire function.
raise UM.VersionUpgrade.InvalidVersionException("The version of this preferences file is wrong. It must be 2.")
if self._config.has_option("general", "name"): #This is probably a machine instance.
raise UM.VersionUpgrade.FormatException("There is a \"name\" field in this configuration file. I suspect it is not a preferences file.")
## Serialises these preferences as a preferences file of version 3.
#
# This is where the actual translation happens.
#
# \return A tuple containing the new filename and a serialised version of
# a preferences file in version 3.
def export(self):
#Reset the cura/categories_expanded property since it works differently now.
if self._config.has_section("cura") and self._config.has_option("cura", "categories_expanded"):
self._config.remove_option("cura", "categories_expanded")
#Translate the setting names in the visible settings.
if self._config.has_section("machines") and self._config.has_option("machines", "setting_visibility"):
visible_settings = self._config.get("machines", "setting_visibility")
visible_settings = visible_settings.split(",")
import VersionUpgrade21to22 #Import here to prevent a circular dependency.
visible_settings = [VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateSettingName(setting_name)
for setting_name in visible_settings]
visible_settings = ",".join(visible_settings)
self._config.set("machines", "setting_visibility", value = visible_settings)
#Translate the active_instance key.
if self._config.has_section("machines") and self._config.has_option("machines", "active_instance"):
active_machine = self._config.get("machines", "active_instance")
self._config.remove_option("machines", "active_instance")
self._config.set("cura", "active_machine", active_machine)
#Update the version number itself.
self._config.set("general", "version", value = "3")
#Output the result as a string.
output = io.StringIO()
self._config.write(output)
return [self._filename], [output.getvalue()]

View file

@ -0,0 +1,159 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
import configparser #To read config files.
import io #To write config files to strings as if they were files.
import UM.VersionUpgrade
## Creates a new profile instance by parsing a serialised profile in version 1
# of the file format.
#
# \param serialised The serialised form of a profile in version 1.
# \param filename The supposed filename of the profile, without extension.
# \return A profile instance, or None if the file format is incorrect.
def importFrom(serialised, filename):
try:
return Profile(serialised, filename)
except (configparser.Error, UM.VersionUpgrade.FormatException, UM.VersionUpgrade.InvalidVersionException):
return None
## A representation of a profile used as intermediary form for conversion from
# one format to the other.
class Profile:
## Reads version 1 of the file format, storing it in memory.
#
# \param serialised A string with the contents of a profile.
# \param filename The supposed filename of the profile, without extension.
def __init__(self, serialised, filename):
self._filename = filename
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
# Check correctness.
if not parser.has_section("general"):
raise UM.VersionUpgrade.FormatException("No \"general\" section.")
if not parser.has_option("general", "version"):
raise UM.VersionUpgrade.FormatException("No \"version\" in the \"general\" section.")
if int(parser.get("general", "version")) != 1: # Hard-coded profile version here. If this number changes the entire function needs to change.
raise UM.VersionUpgrade.InvalidVersionException("The version of this profile is wrong. It must be 1.")
# Parse the general section.
self._name = parser.get("general", "name")
self._type = parser.get("general", "type", fallback = None)
if "weight" in parser["general"]:
self._weight = int(parser.get("general", "weight"))
else:
self._weight = None
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"]:
self._material_name = parser.get("general", "material")
elif self._type == "material":
self._material_name = parser.get("general", "name", fallback = None)
else:
self._material_name = None
# Parse the settings.
self._settings = {}
if parser.has_section("settings"):
for key, value in parser["settings"].items():
self._settings[key] = value
# Parse the defaults and the disabled defaults.
self._changed_settings_defaults = {}
if parser.has_section("defaults"):
for key, value in parser["defaults"].items():
self._changed_settings_defaults[key] = value
self._disabled_settings_defaults = []
if parser.has_section("disabled_defaults"):
disabled_defaults_string = parser.get("disabled_defaults", "values")
self._disabled_settings_defaults = [item for item in disabled_defaults_string.split(",") if item != ""] # Split by comma.
## Serialises this profile as file format version 2.
#
# \return A tuple containing the new filename and a serialised form of
# this profile, serialised in version 2 of the file format.
def export(self):
import VersionUpgrade21to22 # Import here to prevent circular dependencies.
if self._name == "Current settings":
self._filename += "_current_settings" #This resolves a duplicate ID arising from how Cura 2.1 stores its current settings.
config = configparser.ConfigParser(interpolation = None)
config.add_section("general")
config.set("general", "version", "2") #Hard-coded profile version 2.
config.set("general", "name", self._name)
if self._machine_type_id:
translated_machine = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translatePrinter(self._machine_type_id)
config.set("general", "definition", translated_machine)
else:
config.set("general", "definition", "fdmprinter") #In this case, the machine definition is unknown, and it might now have machine-specific profiles, in which case this will fail.
config.add_section("metadata")
if self._type:
config.set("metadata", "type", self._type)
else:
config.set("metadata", "type", "quality")
if self._weight:
config.set("metadata", "weight", self._weight)
if self._machine_variant_name:
if self._machine_type_id:
config.set("metadata", "variant", VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateVariant(self._machine_variant_name, self._machine_type_id))
else:
config.set("metadata", "variant", self._machine_variant_name)
if 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)
config.add_section("defaults")
for key, value in self._changed_settings_defaults.items():
config.set("defaults", key, str(value))
if self._disabled_settings_defaults:
disabled_settings_defaults = [VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateSettingName(setting)
for setting in self._disabled_settings_defaults]
config.add_section("disabled_defaults")
disabled_defaults_string = str(disabled_settings_defaults[0]) #Must be at least 1 item, otherwise we wouldn't enter this if statement.
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

View file

@ -0,0 +1,245 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
import configparser #To get version numbers from config files.
from UM.VersionUpgrade import VersionUpgrade # Superclass of the plugin.
from . import MachineInstance # To upgrade machine instances.
from . import Preferences #To upgrade preferences.
from . import Profile # To upgrade profiles.
## Which machines have material-specific profiles in the new version?
#
# These are the 2.1 machine identities with "has_machine_materials": true in
# their definitions in Cura 2.2. So these are the machines for which profiles
# need to split into multiple profiles, one for each material and variant.
#
# Each machine has the materials and variants listed in which it needs to
# split, since those might be different per machine.
#
# This should contain the definition as they are stated in the profiles. The
# inheritance structure cannot be found at this stage, since the definitions
# may have changed in later versions than 2.2.
_machines_with_machine_quality = {
"ultimaker2plus": {
"materials": { "generic_abs", "generic_cpe", "generic_pla", "generic_pva" },
"variants": { "0.25 mm", "0.4 mm", "0.6 mm", "0.8 mm" }
},
"ultimaker2_extended_plus": {
"materials": { "generic_abs", "generic_cpe", "generic_pla", "generic_pva" },
"variants": { "0.25 mm", "0.4 mm", "0.6 mm", "0.8 mm" }
}
}
## How to translate printer names from the old version to the new.
_printer_translations = {
"ultimaker2plus": "ultimaker2_plus"
}
## How to translate profile names from the old version to the new.
_profile_translations = {
"PLA": "generic_pla",
"ABS": "generic_abs",
"CPE": "generic_cpe",
"Low Quality": "low",
"Normal Quality": "normal",
"High Quality": "high",
"Ulti Quality": "high" #This one doesn't have an equivalent. Map it to high.
}
## How to translate setting names from the old version to the new.
_setting_name_translations = {
"remove_overlapping_walls_0_enabled": "travel_compensate_overlapping_walls_0_enabled",
"remove_overlapping_walls_enabled": "travel_compensate_overlapping_walls_enabled",
"remove_overlapping_walls_x_enabled": "travel_compensate_overlapping_walls_x_enabled",
"retraction_hop": "retraction_hop_enabled",
"skirt_line_width": "skirt_brim_line_width",
"skirt_minimal_length": "skirt_brim_minimal_length",
"skirt_speed": "skirt_brim_speed",
"speed_support_lines": "speed_support_infill",
"speed_support_roof": "speed_support_interface",
"support_roof_density": "support_interface_density",
"support_roof_enable": "support_interface_enable",
"support_roof_extruder_nr": "support_interface_extruder_nr",
"support_roof_line_distance": "support_interface_line_distance",
"support_roof_line_width": "support_interface_line_width",
"support_roof_pattern": "support_interface_pattern"
}
## How to translate variants of specific machines from the old version to the
# new.
_variant_translations = {
"ultimaker2_plus": {
"0.25 mm": "ultimaker2_plus_0.25",
"0.4 mm": "ultimaker2_plus_0.4",
"0.6 mm": "ultimaker2_plus_0.6",
"0.8 mm": "ultimaker2_plus_0.8"
},
"ultimaker2_extended_plus": {
"0.25 mm": "ultimaker2_extended_plus_0.25",
"0.4 mm": "ultimaker2_extended_plus_0.4",
"0.6 mm": "ultimaker2_extended_plus_0.6",
"0.8 mm": "ultimaker2_extended_plus_0.8"
}
}
## Cura 2.2's material profiles use a different naming scheme for variants.
#
# Getting pretty stressed out by this sort of thing...
_variant_translations_materials = {
"ultimaker2_plus": {
"0.25 mm": "ultimaker2_plus_0.25_mm",
"0.4 mm": "ultimaker2_plus_0.4_mm",
"0.6 mm": "ultimaker2_plus_0.6_mm",
"0.8 mm": "ultimaker2_plus_0.8_mm"
},
"ultimaker2_extended_plus": {
"0.25 mm": "ultimaker2_plus_0.25_mm",
"0.4 mm": "ultimaker2_plus_0.4_mm",
"0.6 mm": "ultimaker2_plus_0.6_mm",
"0.8 mm": "ultimaker2_plus_0.8_mm"
}
}
## Converts configuration from Cura 2.1's file formats to Cura 2.2's.
#
# It converts the machine instances and profiles.
class VersionUpgrade21to22(VersionUpgrade):
## Gets the version number from a config file.
#
# In all config files that concern this version upgrade, the version
# number is stored in general/version, so get the data from that key.
#
# \param serialised The contents of a config file.
# \return \type{int} The version number of that config file.
def getCfgVersion(self, serialised):
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
return int(parser.get("general", "version")) #Explicitly give an exception when this fails. That means that the file format is not recognised.
## Gets a set of the machines which now have per-material quality profiles.
#
# \return A set of machine identifiers.
@staticmethod
def machinesWithMachineQuality():
return _machines_with_machine_quality
## Converts machine instances from format version 1 to version 2.
#
# \param serialised The serialised machine instance in version 1.
# \param filename The supposed file name of the machine instance, without
# extension.
# \return A tuple containing the new filename and the serialised machine
# instance in version 2, or None if the input was not of the correct
# format.
def upgradeMachineInstance(self, serialised, filename):
machine_instance = MachineInstance.importFrom(serialised, filename)
if not machine_instance: #Invalid file format.
return filename, None
return machine_instance.export()
## Converts preferences from format version 2 to version 3.
#
# \param serialised The serialised preferences file in version 2.
# \param filename THe supposed file name of the preferences file, without
# extension.
# \return A tuple containing the new filename and the serialised
# preferences in version 3, or None if the input was not of the correct
# format.
def upgradePreferences(self, serialised, filename):
preferences = Preferences.importFrom(serialised, filename)
if not preferences: #Invalid file format.
return filename, None
return preferences.export()
## Converts profiles from format version 1 to version 2.
#
# \param serialised The serialised profile in version 1.
# \param filename The supposed file name of the profile, without
# extension.
# \return A tuple containing the new filename and the serialised profile
# in version 2, or None if the input was not of the correct format.
def upgradeProfile(self, serialised, filename):
profile = Profile.importFrom(serialised, filename)
if not profile: # Invalid file format.
return filename, None
return profile.export()
## Translates a printer name that might have changed since the last
# version.
#
# \param printer A printer name in Cura 2.1.
# \return The name of the corresponding printer in Cura 2.2.
@staticmethod
def translatePrinter(printer):
if printer in _printer_translations:
return _printer_translations[printer]
return printer #Doesn't need to be translated.
## Translates a built-in profile name that might have changed since the
# last version.
#
# \param profile A profile name in the old version.
# \return The corresponding profile name in the new version.
@staticmethod
def translateProfile(profile):
if profile in _profile_translations:
return _profile_translations[profile]
return profile #Doesn't need to be translated.
## Updates settings for the change from Cura 2.1 to 2.2.
#
# The keys and values of settings are changed to what they should be in
# the new version. Each setting is changed in-place in the provided
# dictionary. This changes the input parameter.
#
# \param settings A dictionary of settings (as key-value pairs) to update.
# \return The same dictionary.
@staticmethod
def translateSettings(settings):
for key, value in settings.items():
if key == "fill_perimeter_gaps": #Setting is removed.
del settings[key]
elif key == "retraction_combing": #Combing was made into an enum instead of a boolean.
settings[key] = "off" if (value == "False") else "all"
elif key in _setting_name_translations:
del settings[key]
settings[_setting_name_translations[key]] = value
return settings
## Translates a setting name for the change from Cura 2.1 to 2.2.
#
# \param setting The name of a setting in Cura 2.1.
# \return The name of the corresponding setting in Cura 2.2.
@staticmethod
def translateSettingName(setting):
if setting in _setting_name_translations:
return _setting_name_translations[setting]
return setting #Doesn't need to be translated.
## Translates a variant name for the change from Cura 2.1 to 2.2
#
# \param variant The name of a variant in Cura 2.1.
# \param machine The name of the machine this variant is part of in Cura
# 2.2's naming.
# \return The name of the corresponding variant in Cura 2.2.
@staticmethod
def translateVariant(variant, machine):
if machine in _variant_translations and variant in _variant_translations[machine]:
return _variant_translations[machine][variant]
return variant
## Translates a variant name for the change from Cura 2.1 to 2.2 in
# material profiles.
#
# \param variant The name of the variant in Cura 2.1.
# \param machine The name of the machine this variant is part of in Cura
# 2.2's naming.
# \return The name of the corresponding variant for in material profiles
# in Cura 2.2.
@staticmethod
def translateVariantForMaterials(variant, machine):
if machine in _variant_translations_materials and variant in _variant_translations_materials[machine]:
return _variant_translations_materials[machine][variant]
return variant

View file

@ -0,0 +1,43 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from . import VersionUpgrade21to22
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
upgrade = VersionUpgrade21to22.VersionUpgrade21to22()
def getMetaData():
return {
"plugin": {
"name": catalog.i18nc("@label", "Version Upgrade 2.1 to 2.2"),
"author": "Ultimaker",
"version": "1.0",
"description": catalog.i18nc("@info:whatsthis", "Upgrades configurations from Cura 2.1 to Cura 2.2."),
"api": 3
},
"version_upgrade": {
# From To Upgrade function
("profile", 1): ("quality", 2, upgrade.upgradeProfile),
("machine_instance", 1): ("machine_stack", 2, upgrade.upgradeMachineInstance),
("preferences", 2): ("preferences", 3, upgrade.upgradePreferences)
},
"sources": {
"profile": {
"get_version": upgrade.getCfgVersion,
"location": {"./profiles", "./instance_profiles"}
},
"machine_instance": {
"get_version": upgrade.getCfgVersion,
"location": {"./machine_instances"}
},
"preferences": {
"get_version": upgrade.getCfgVersion,
"location": {"."}
}
}
}
def register(app):
return { "version_upgrade": upgrade }

View file

@ -0,0 +1,39 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
import os.path
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 UM.Scene.SceneNode import SceneNode
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
class XRayPass(RenderPass):
def __init__(self, width, height):
super().__init__("xray", width, height)
self._shader = None
self._gl = OpenGL.getInstance().getBindingsObject()
self._scene = Application.getInstance().getController().getScene()
def render(self):
if not self._shader:
self._shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("XRayView"), "xray.shader"))
batch = RenderBatch(self._shader, type = RenderBatch.RenderType.NoType, backface_cull = False, blend_mode = RenderBatch.BlendMode.Additive)
for node in DepthFirstIterator(self._scene.getRoot()):
if type(node) is SceneNode and node.getMeshData() and node.isVisible():
batch.addItem(node.getWorldTransformation(), node.getMeshData())
self.bind()
self._gl.glDisable(self._gl.GL_DEPTH_TEST)
batch.render(self._scene.getActiveCamera())
self._gl.glEnable(self._gl.GL_DEPTH_TEST)
self.release()

View file

@ -0,0 +1,72 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
import os.path
from UM.PluginRegistry import PluginRegistry
from UM.Event import Event
from UM.View.View import View
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.View.RenderBatch import RenderBatch
from UM.View.GL.OpenGL import OpenGL
from . import XRayPass
## View used to display a see-through version of objects with errors highlighted.
class XRayView(View):
def __init__(self):
super().__init__()
self._xray_shader = None
self._xray_pass = None
self._xray_composite_shader = None
self._composite_pass = None
self._old_composite_shader = None
self._old_layer_bindings = None
def beginRendering(self):
scene = self.getController().getScene()
renderer = self.getRenderer()
if not self._xray_shader:
self._xray_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("XRayView"), "xray.shader"))
self._xray_shader.setUniformValue("u_color", [0.1, 0.1, 0.2, 1.0])
for node in BreadthFirstIterator(scene.getRoot()):
if not node.render(renderer):
if node.getMeshData() and node.isVisible():
renderer.queueNode(node,
shader = self._xray_shader,
type = RenderBatch.RenderType.Solid,
blend_mode = RenderBatch.BlendMode.Additive,
sort = -10,
state_setup_callback = lambda gl: gl.glDepthFunc(gl.GL_ALWAYS),
state_teardown_callback = lambda gl: gl.glDepthFunc(gl.GL_LESS)
)
def endRendering(self):
pass
def event(self, event):
if event.type == Event.ViewActivateEvent:
if not self._xray_pass:
# Currently the RenderPass constructor requires a size > 0
# This should be fixed in RenderPass's constructor.
self._xray_pass = XRayPass.XRayPass(1, 1)
self.getRenderer().addRenderPass(self._xray_pass)
if not self._xray_composite_shader:
self._xray_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("XRayView"), "xray_composite.shader"))
if not self._composite_pass:
self._composite_pass = self.getRenderer().getRenderPass("composite")
self._old_layer_bindings = self._composite_pass.getLayerBindings()
self._composite_pass.setLayerBindings(["default", "selection", "xray"])
self._old_composite_shader = self._composite_pass.getCompositeShader()
self._composite_pass.setCompositeShader(self._xray_composite_shader)
if event.type == Event.ViewDeactivateEvent:
self._composite_pass.setLayerBindings(self._old_layer_bindings)
self._composite_pass.setCompositeShader(self._old_composite_shader)

View file

@ -0,0 +1,25 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from . import XRayView
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {
"plugin": {
"name": catalog.i18nc("@label", "X-Ray View"),
"author": "Ultimaker",
"version": "1.0",
"description": catalog.i18nc("@info:whatsthis", "Provides the X-Ray view."),
"api": 3
},
"view": {
"name": catalog.i18nc("@item:inlistbox", "X-Ray"),
"weight": 1
}
}
def register(app):
return { "view": XRayView.XRayView() }

View file

@ -0,0 +1,27 @@
[shaders]
vertex =
uniform highp mat4 u_modelViewProjectionMatrix;
attribute highp vec4 a_vertex;
void main()
{
gl_Position = u_modelViewProjectionMatrix * a_vertex;
}
fragment =
uniform lowp vec4 u_color;
void main()
{
gl_FragColor = u_color;
}
[defaults]
u_color = [0.02, 0.02, 0.02, 1.0]
[bindings]
u_modelViewProjectionMatrix = model_view_projection_matrix
[attributes]
a_vertex = vertex

View file

@ -0,0 +1,75 @@
[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 sampler2D u_layer3;
uniform float u_imageWidth;
uniform float u_imageHeight;
uniform vec2 u_offset[9];
uniform float u_outline_strength;
uniform vec4 u_outline_color;
uniform vec4 u_error_color;
varying vec2 v_uvs;
float kernel[9];
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 = vec4(0.965, 0.965, 0.965, 1.0);
vec4 layer0 = texture2D(u_layer0, v_uvs);
result = layer0 * layer0.a + result * (1.0 - layer0.a);
float intersection_count = (texture2D(u_layer2, v_uvs).r * 255.0) / 5.0;
if(mod(intersection_count, 2.0) == 1.0)
{
result = u_error_color;
}
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);
}
gl_FragColor = mix(result, vec4(abs(sum.a)) * u_outline_color, abs(sum.a));
}
[defaults]
u_layer0 = 0
u_layer1 = 1
u_layer2 = 2
u_layer3 = 3
u_outline_strength = 1.0
u_outline_color = [0.05, 0.66, 0.89, 1.0]
u_error_color = [1.0, 0.0, 0.0, 1.0]
[bindings]
[attributes]
a_vertex = vertex
a_uvs = uv

View file

@ -0,0 +1,420 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
import math
import copy
import io
import xml.etree.ElementTree as ET
import uuid
from UM.Logger import Logger
import UM.Dictionary
import UM.Settings
## Handles serializing and deserializing material containers from an XML file
class XmlMaterialProfile(UM.Settings.InstanceContainer):
def __init__(self, container_id, *args, **kwargs):
super().__init__(container_id, *args, **kwargs)
## Overridden from InstanceContainer
def duplicate(self, new_id, new_name = None):
base_file = self.getMetaDataEntry("base_file", None)
new_uuid = str(uuid.uuid4())
if base_file:
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = base_file)
if containers:
new_basefile = containers[0].duplicate(self.getMetaDataEntry("brand") + "_" + new_id, new_name)
new_basefile.setMetaDataEntry("GUID", new_uuid)
base_file = new_basefile.id
UM.Settings.ContainerRegistry.getInstance().addContainer(new_basefile)
new_id = self.getMetaDataEntry("brand") + "_" + new_id + "_" + self.getDefinition().getId()
variant = self.getMetaDataEntry("variant")
if variant:
variant_containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = variant)
if variant_containers:
new_id += "_" + variant_containers[0].getName().replace(" ", "_")
result = super().duplicate(new_id, new_name)
result.setMetaDataEntry("GUID", new_uuid)
if result.getMetaDataEntry("base_file", None):
result.setMetaDataEntry("base_file", base_file)
return result
## Overridden from InstanceContainer
def setReadOnly(self, read_only):
super().setReadOnly(read_only)
for container in UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(GUID = self.getMetaDataEntry("GUID")):
container._read_only = read_only
## Overridden from InstanceContainer
def setMetaDataEntry(self, key, value):
if self.isReadOnly():
return
super().setMetaDataEntry(key, value)
if key == "material":
self.setName(value)
for container in UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(GUID = self.getMetaDataEntry("GUID")):
container.setMetaData(copy.deepcopy(self._metadata))
if key == "material":
container.setName(value)
## Overridden from InstanceContainer
def setProperty(self, key, property_name, property_value, container = None):
if self.isReadOnly():
return
super().setProperty(key, property_name, property_value)
for container in UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(GUID = self.getMetaDataEntry("GUID")):
container._dirty = True
## Overridden from InstanceContainer
def serialize(self):
registry = UM.Settings.ContainerRegistry.getInstance()
base_file = self.getMetaDataEntry("base_file", "")
if base_file and self.id != base_file:
# Since we create an instance of XmlMaterialProfile for each machine and nozzle in the profile,
# we should only serialize the "base" material definition, since that can then take care of
# serializing the machine/nozzle specific profiles.
raise NotImplementedError("Cannot serialize non-root XML materials")
builder = ET.TreeBuilder()
root = builder.start("fdmmaterial", { "xmlns": "http://www.ultimaker.com/material"})
## Begin Metadata Block
builder.start("metadata")
metadata = copy.deepcopy(self.getMetaData())
properties = metadata.pop("properties", {})
# Metadata properties that should not be serialized.
metadata.pop("status", "")
metadata.pop("variant", "")
metadata.pop("type", "")
metadata.pop("base_file", "")
## Begin Name Block
builder.start("name")
builder.start("brand")
builder.data(metadata.pop("brand", ""))
builder.end("brand")
builder.start("material")
builder.data(metadata.pop("material", ""))
builder.end("material")
builder.start("color")
builder.data(metadata.pop("color_name", ""))
builder.end("color")
builder.end("name")
## End Name Block
for key, value in metadata.items():
builder.start(key)
builder.data(value)
builder.end(key)
builder.end("metadata")
## End Metadata Block
## Begin Properties Block
builder.start("properties")
for key, value in properties.items():
builder.start(key)
builder.data(value)
builder.end(key)
builder.end("properties")
## End Properties Block
## Begin Settings Block
builder.start("settings")
if self.getDefinition().id == "fdmprinter":
for instance in self.findInstances():
self._addSettingElement(builder, instance)
machine_container_map = {}
machine_nozzle_map = {}
all_containers = registry.findInstanceContainers(GUID = self.getMetaDataEntry("GUID"))
for container in all_containers:
definition_id = container.getDefinition().id
if definition_id == "fdmprinter":
continue
if definition_id not in machine_container_map:
machine_container_map[definition_id] = container
if definition_id not in machine_nozzle_map:
machine_nozzle_map[definition_id] = {}
variant = container.getMetaDataEntry("variant")
if variant:
machine_nozzle_map[definition_id][variant] = container
continue
machine_container_map[definition_id] = container
for definition_id, container in machine_container_map.items():
definition = container.getDefinition()
try:
product = UM.Dictionary.findKey(self.__product_id_map, definition_id)
except ValueError:
continue
builder.start("machine")
builder.start("machine_identifier", { "manufacturer": definition.getMetaDataEntry("manufacturer", ""), "product": product})
builder.end("machine_identifier")
for instance in container.findInstances():
if self.getDefinition().id == "fdmprinter" and self.getInstance(instance.definition.key) and self.getProperty(instance.definition.key, "value") == instance.value:
# If the settings match that of the base profile, just skip since we inherit the base profile.
continue
self._addSettingElement(builder, instance)
# Find all hotend sub-profiles corresponding to this material and machine and add them to this profile.
for hotend_id, hotend in machine_nozzle_map[definition_id].items():
variant_containers = registry.findInstanceContainers(id = hotend.getMetaDataEntry("variant"))
if not variant_containers:
continue
builder.start("hotend", { "id": variant_containers[0].getName() })
for instance in hotend.findInstances():
if container.getInstance(instance.definition.key) and container.getProperty(instance.definition.key, "value") == instance.value:
# If the settings match that of the machine profile, just skip since we inherit the machine profile.
continue
self._addSettingElement(builder, instance)
builder.end("hotend")
builder.end("machine")
builder.end("settings")
## End Settings Block
builder.end("fdmmaterial")
root = builder.close()
_indent(root)
stream = io.StringIO()
tree = ET.ElementTree(root)
tree.write(stream, "unicode", True)
return stream.getvalue()
## Overridden from InstanceContainer
def deserialize(self, serialized):
data = ET.fromstring(serialized)
self.addMetaDataEntry("type", "material")
# TODO: Add material verfication
self.addMetaDataEntry("status", "unknown")
metadata = data.iterfind("./um:metadata/*", self.__namespaces)
for entry in metadata:
tag_name = _tag_without_namespace(entry)
if tag_name == "name":
brand = entry.find("./um:brand", self.__namespaces)
material = entry.find("./um:material", self.__namespaces)
color = entry.find("./um:color", self.__namespaces)
self.setName(material.text)
self.addMetaDataEntry("brand", brand.text)
self.addMetaDataEntry("material", material.text)
self.addMetaDataEntry("color_name", color.text)
continue
self.addMetaDataEntry(tag_name, entry.text)
if not "description" in self.getMetaData():
self.addMetaDataEntry("description", "")
if not "adhesion_info" in self.getMetaData():
self.addMetaDataEntry("adhesion_info", "")
property_values = {}
properties = data.iterfind("./um:properties/*", self.__namespaces)
for entry in properties:
tag_name = _tag_without_namespace(entry)
property_values[tag_name] = entry.text
diameter = float(property_values.get("diameter", 2.85)) # In mm
density = float(property_values.get("density", 1.3)) # In g/cm3
self.addMetaDataEntry("properties", property_values)
self.setDefinition(UM.Settings.ContainerRegistry.getInstance().findDefinitionContainers(id = "fdmprinter")[0])
global_setting_values = {}
settings = data.iterfind("./um:settings/um:setting", self.__namespaces)
for entry in settings:
key = entry.get("key")
if key in self.__material_property_setting_map:
self.setProperty(self.__material_property_setting_map[key], "value", entry.text, self._definition)
global_setting_values[self.__material_property_setting_map[key]] = entry.text
else:
Logger.log("d", "Unsupported material setting %s", key)
self._dirty = False
machines = data.iterfind("./um:settings/um:machine", self.__namespaces)
for machine in machines:
machine_setting_values = {}
settings = machine.iterfind("./um:setting", self.__namespaces)
for entry in settings:
key = entry.get("key")
if key in self.__material_property_setting_map:
machine_setting_values[self.__material_property_setting_map[key]] = entry.text
else:
Logger.log("d", "Unsupported material setting %s", key)
identifiers = machine.iterfind("./um:machine_identifier", self.__namespaces)
for identifier in identifiers:
machine_id = self.__product_id_map.get(identifier.get("product"), None)
if machine_id is None:
Logger.log("w", "Cannot create material for unknown machine %s", machine_id)
continue
definitions = UM.Settings.ContainerRegistry.getInstance().findDefinitionContainers(id = machine_id)
if not definitions:
Logger.log("w", "No definition found for machine ID %s", machine_id)
continue
definition = definitions[0]
new_material = XmlMaterialProfile(self.id + "_" + machine_id)
new_material.setName(self.getName())
new_material.setMetaData(copy.deepcopy(self.getMetaData()))
new_material.setDefinition(definition)
new_material.addMetaDataEntry("base_file", self.id)
for key, value in global_setting_values.items():
new_material.setProperty(key, "value", value, definition)
for key, value in machine_setting_values.items():
new_material.setProperty(key, "value", value, definition)
new_material._dirty = False
UM.Settings.ContainerRegistry.getInstance().addContainer(new_material)
hotends = machine.iterfind("./um:hotend", self.__namespaces)
for hotend in hotends:
hotend_id = hotend.get("id")
if hotend_id is None:
continue
variant_containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = hotend_id)
if not variant_containers:
# It is not really properly defined what "ID" is so also search for variants by name.
variant_containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(definition = definition.id, name = hotend_id)
if not variant_containers:
Logger.log("d", "No variants found with ID or name %s for machine %s", hotend_id, definition.id)
continue
new_hotend_material = XmlMaterialProfile(self.id + "_" + machine_id + "_" + hotend_id.replace(" ", "_"))
new_hotend_material.setName(self.getName())
new_hotend_material.setMetaData(copy.deepcopy(self.getMetaData()))
new_hotend_material.setDefinition(definition)
new_hotend_material.addMetaDataEntry("base_file", self.id)
new_hotend_material.addMetaDataEntry("variant", variant_containers[0].id)
for key, value in global_setting_values.items():
new_hotend_material.setProperty(key, "value", value, definition)
for key, value in machine_setting_values.items():
new_hotend_material.setProperty(key, "value", value, definition)
settings = hotend.iterfind("./um:setting", self.__namespaces)
for entry in settings:
key = entry.get("key")
if key in self.__material_property_setting_map:
new_hotend_material.setProperty(self.__material_property_setting_map[key], "value", entry.text, definition)
else:
Logger.log("d", "Unsupported material setting %s", key)
new_hotend_material._dirty = False
UM.Settings.ContainerRegistry.getInstance().addContainer(new_hotend_material)
def _addSettingElement(self, builder, instance):
try:
key = UM.Dictionary.findKey(self.__material_property_setting_map, instance.definition.key)
except ValueError:
return
builder.start("setting", { "key": key })
builder.data(str(instance.value))
builder.end("setting")
# Map XML file setting names to internal names
__material_property_setting_map = {
"print temperature": "material_print_temperature",
"heated bed temperature": "material_bed_temperature",
"standby temperature": "material_standby_temperature",
"print cooling": "cool_fan_speed",
"retraction amount": "retraction_amount",
"retraction speed": "retraction_speed",
}
# Map XML file product names to internal ids
# TODO: Move this to definition's metadata
__product_id_map = {
"Ultimaker2": "ultimaker2",
"Ultimaker2+": "ultimaker2_plus",
"Ultimaker2go": "ultimaker2_go",
"Ultimaker2extended": "ultimaker2_extended",
"Ultimaker2extended+": "ultimaker2_extended_plus",
"Ultimaker Original": "ultimaker_original",
"Ultimaker Original+": "ultimaker_original_plus"
}
# Map of recognised namespaces with a proper prefix.
__namespaces = {
"um": "http://www.ultimaker.com/material"
}
## Helper function for pretty-printing XML because ETree is stupid
def _indent(elem, level = 0):
i = "\n" + level * " "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
_indent(elem, level + 1)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
# The namespace is prepended to the tag name but between {}.
# We are only interested in the actual tag name, so discard everything
# before the last }
def _tag_without_namespace(element):
return element.tag[element.tag.rfind("}") + 1:]

View file

@ -0,0 +1,33 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from . import XmlMaterialProfile
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {
"plugin": {
"name": catalog.i18nc("@label", "Material Profiles"),
"author": "Ultimaker",
"version": "1.0",
"description": catalog.i18nc("@info:whatsthis", "Provides capabilities to read and write XML-based material profiles."),
"api": 3
},
"settings_container": {
"type": "material",
"mimetype": "application/x-ultimaker-material-profile"
}
}
def register(app):
mime_type = MimeType(
name = "application/x-ultimaker-material-profile",
comment = "Ultimaker Material Profile",
suffixes = [ "xml.fdm_material" ]
)
MimeTypeDatabase.addMimeType(mime_type)
return { "settings_container": XmlMaterialProfile.XmlMaterialProfile("default_xml_material_profile") }