diff --git a/CHANGES b/CHANGES index 64858ff8ea..bd9c26751c 100644 --- a/CHANGES +++ b/CHANGES @@ -7,6 +7,22 @@ Cura 15.06 is a new release built from the ground up on a completely new framework called Uranium. This framework has been designed to make it easier to extend Cura with additional functionality as well as provide a cleaner UI. +Changes since 15.05.95 +---------------------- + +* Fixed: Selection ghost remains visible after deleting an object +* Fixed: Window does not show up immediately after starting application on OSX +* Fixed: Added display of rotation angle during rotation +* Fixed: Object changes position while rotating/scaling +* Fixed: Loading improvements in the layer view +* Fixed: Added application icons +* Fixed: Improved feedback when loading models +* Fixed: Eject device on MacOSX now provides proper feedback +* Fixed: Make it possible to show retraction settings for UM2 +* Fixed: Opening the machine preferences page will switch to the first available machine +* Fixed: Improved tool handle hit area size +* Fixed: Render lines with a thickness based on screen DPI + Changes since 15.05.94 ---------------------- @@ -150,18 +166,8 @@ For an up to date list of all known issues, please see https://github.com/Ultimaker/Cura/issues and https://github.com/Ultimaker/Uranium/issues . -* The application has no application icon yet. -* The Windows version starts a console before starting the - application. This is intentional for the beta and it will be - removed for the final version. -* Opening the machine preferences page will switch to the first - available machine instead of keeping the current machine - selected. * Some OBJ files are rendered as black objects due to missing normals. -* The developer documentation for Uranium (available at - http://software.ultimaker.com/uranium/index.html) is not yet - complete. * Disabling plugins does not work correctly yet. * Unicorn occasionally still requires feeding. Do not feed it after midnight. diff --git a/cura/CuraActions.py b/cura/CuraActions.py index ee75665e0b..e585b261d0 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -1,4 +1,8 @@ -from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty +from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty, QUrl +from PyQt5.QtGui import QDesktopServices + +from UM.Event import CallFunctionEvent +from UM.Application import Application import webbrowser @@ -8,8 +12,16 @@ class CuraActions(QObject): @pyqtSlot() def openDocumentation(self): - webbrowser.open("http://ultimaker.com/en/support/software") + # Starting a web browser from a signal handler connected to a menu will crash on windows. + # So instead, defer the call to the next run of the event loop, since that does work. + # Note that weirdly enough, only signal handlers that open a web browser fail like that. + event = CallFunctionEvent(self._openUrl, [QUrl("http://ultimaker.com/en/support/software")], {}) + Application.getInstance().functionEvent(event) @pyqtSlot() def openBugReportPage(self): - webbrowser.open("http://github.com/Ultimaker/Cura/issues") + event = CallFunctionEvent(self._openUrl, [QUrl("http://github.com/Ultimaker/Cura/issues")], {}) + Application.getInstance().functionEvent(event) + + def _openUrl(self, url): + QDesktopServices.openUrl(url) \ No newline at end of file diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index b7ac5f96c8..78ef677556 100644 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -85,7 +85,7 @@ class CuraApplication(QtApplication): if not os.path.isfile(f): continue - self._recent_files.append(f) + self._recent_files.append(QUrl.fromLocalFile(f)) ## Handle loading of all plugin types (and the backend explicitly) # \sa PluginRegistery @@ -215,6 +215,9 @@ class CuraApplication(QtApplication): def deleteObject(self, object_id): object = self.getController().getScene().findObject(object_id) + if not object and object_id != 0: #Workaround for tool handles overlapping the selected object + object = Selection.getSelectedObject(0) + if object: op = RemoveSceneNodeOperation(object) op.push() @@ -224,6 +227,9 @@ class CuraApplication(QtApplication): def multiplyObject(self, object_id, count): node = self.getController().getScene().findObject(object_id) + if not node and object_id != 0: #Workaround for tool handles overlapping the selected object + node = Selection.getSelectedObject(0) + if node: op = GroupedOperation() for i in range(count): @@ -240,6 +246,9 @@ class CuraApplication(QtApplication): def centerObject(self, object_id): node = self.getController().getScene().findObject(object_id) + if not node and object_id != 0: #Workaround for tool handles overlapping the selected object + node = Selection.getSelectedObject(0) + if node: op = SetTransformOperation(node, Vector()) op.push() @@ -330,7 +339,7 @@ class CuraApplication(QtApplication): return log recentFilesChanged = pyqtSignal() - @pyqtProperty("QStringList", notify = recentFilesChanged) + @pyqtProperty("QVariantList", notify = recentFilesChanged) def recentFiles(self): return self._recent_files @@ -468,7 +477,9 @@ class CuraApplication(QtApplication): self._volume.rebuild() if self.getController().getTool("ScaleTool"): - self.getController().getTool("ScaleTool").setMaximumBounds(self._volume.getBoundingBox()) + bbox = self._volume.getBoundingBox() + bbox.setBottom(0.0) + self.getController().getTool("ScaleTool").setMaximumBounds(bbox) offset = machine.getSettingValueByKey("machine_platform_offset") if offset: @@ -508,7 +519,7 @@ class CuraApplication(QtApplication): if type(job) is not ReadMeshJob: return - f = job.getFileName() + f = QUrl.fromLocalFile(job.getFileName()) if f in self._recent_files: self._recent_files.remove(f) @@ -516,5 +527,9 @@ class CuraApplication(QtApplication): if len(self._recent_files) > 10: del self._recent_files[10] - Preferences.getInstance().setValue("cura/recent_files", ";".join(self._recent_files)) + pref = "" + for path in self._recent_files: + pref += path.toLocalFile() + ";" + + Preferences.getInstance().setValue("cura/recent_files", pref) self.recentFilesChanged.emit() diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index 5e4bd5a415..d7276c9773 100644 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -24,8 +24,12 @@ class PlatformPhysics: super().__init__() self._controller = controller self._controller.getScene().sceneChanged.connect(self._onSceneChanged) + self._controller.toolOperationStarted.connect(self._onToolOperationStarted) + self._controller.toolOperationStopped.connect(self._onToolOperationStopped) self._build_volume = volume + self._enabled = True + self._change_timer = QTimer() self._change_timer.setInterval(100) self._change_timer.setSingleShot(True) @@ -35,6 +39,9 @@ class PlatformPhysics: self._change_timer.start() def _onChangeTimerFinished(self): + if not self._enabled: + return + root = self._controller.getScene().getRoot() for node in BreadthFirstIterator(root): if node is root or type(node) is not SceneNode: @@ -93,3 +100,10 @@ class PlatformPhysics: if node.getBoundingBox().intersectsBox(self._build_volume.getBoundingBox()) == AxisAlignedBox.IntersectionResult.FullIntersection: op = ScaleToBoundsOperation(node, self._build_volume.getBoundingBox()) op.push() + + def _onToolOperationStarted(self, tool): + self._enabled = False + + def _onToolOperationStopped(self, tool): + self._enabled = True + self._onChangeTimerFinished() diff --git a/icons/cura-128.png b/icons/cura-128.png new file mode 100644 index 0000000000..ecfe0d1e4e Binary files /dev/null and b/icons/cura-128.png differ diff --git a/icons/cura-32.png b/icons/cura-32.png new file mode 100644 index 0000000000..2ad22313e9 Binary files /dev/null and b/icons/cura-32.png differ diff --git a/icons/cura-48.png b/icons/cura-48.png new file mode 100644 index 0000000000..5fadb70f0f Binary files /dev/null and b/icons/cura-48.png differ diff --git a/icons/cura-64.png b/icons/cura-64.png new file mode 100644 index 0000000000..b6abad9ac1 Binary files /dev/null and b/icons/cura-64.png differ diff --git a/icons/cura.icns b/icons/cura.icns new file mode 100644 index 0000000000..eac1092e0e Binary files /dev/null and b/icons/cura.icns differ diff --git a/icons/cura.ico b/icons/cura.ico new file mode 100644 index 0000000000..c6b554301c Binary files /dev/null and b/icons/cura.ico differ diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 45a2148892..9fbd7a47a1 100644 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -59,6 +59,8 @@ class CuraEngineBackend(Backend): self._save_polygons = True self._report_progress = True + self._enabled = True + self.backendConnected.connect(self._onBackendConnected) def getEngineCommand(self): @@ -86,6 +88,9 @@ class CuraEngineBackend(Backend): # If False, this method will do nothing when already slicing. True by default. # - report_progress: True if the slicing progress should be reported, False if not. Default is True. def slice(self, **kwargs): + if not self._enabled: + return + if self._slicing: if not kwargs.get("force_restart", True): return @@ -235,3 +240,10 @@ class CuraEngineBackend(Backend): if self._restart: self._onChanged() self._restart = False + + def _onToolOperationStarted(self, tool): + self._enabled = False + + def _onToolOperationStopped(self, tool): + self._enabled = True + self._onChanged() diff --git a/plugins/CuraEngineBackend/LayerData.py b/plugins/CuraEngineBackend/LayerData.py index b129942c36..c793c17504 100644 --- a/plugins/CuraEngineBackend/LayerData.py +++ b/plugins/CuraEngineBackend/LayerData.py @@ -8,6 +8,7 @@ from UM.Math.Vector import Vector import numpy import math +import copy class LayerData(MeshData): def __init__(self): @@ -48,11 +49,23 @@ class LayerData(MeshData): self._layers[layer].setThickness(thickness) def build(self): + vertex_count = 0 for layer, data in self._layers.items(): - data.build() + vertex_count += data.vertexCount() + vertices = numpy.empty((vertex_count, 3), numpy.float32) + colors = numpy.empty((vertex_count, 4), numpy.float32) + indices = numpy.empty((vertex_count, 2), numpy.int32) + + offset = 0 + for layer, data in self._layers.items(): + offset = data.build(offset, vertices, colors, indices) self._element_counts[layer] = data.elementCount + self.addVertices(vertices) + self.addColors(colors) + self.addIndices(indices.flatten()) + class Layer(): def __init__(self, id): self._id = id @@ -83,20 +96,30 @@ class Layer(): def setThickness(self, thickness): self._thickness = thickness - def build(self): + def vertexCount(self): + result = 0 + for polygon in self._polygons: + result += polygon.vertexCount() + + return result + + def build(self, offset, vertices, colors, indices): + result = offset for polygon in self._polygons: if polygon._type == Polygon.InfillType or polygon._type == Polygon.SupportInfillType: continue - polygon.build() + polygon.build(result, vertices, colors, indices) + result += polygon.vertexCount() self._element_count += polygon.elementCount + return result + def createMesh(self): builder = MeshBuilder() for polygon in self._polygons: poly_color = polygon.getColor() - poly_color = Color(poly_color[0], poly_color[1], poly_color[2], poly_color[3]) points = numpy.copy(polygon.data) if polygon.type == Polygon.InfillType or polygon.type == Polygon.SkinType or polygon.type == Polygon.SupportInfillType: @@ -159,43 +182,48 @@ class Polygon(): self._data = data self._line_width = line_width / 1000 - def build(self): - self._begin = self._mesh._vertex_count - self._mesh.addVertices(self._data) - self._end = self._begin + len(self._data) - 1 + def build(self, offset, vertices, colors, indices): + self._begin = offset color = self.getColor() - color[3] = 2.0 + color.setValues(color.r * 0.5, color.g * 0.5, color.b * 0.5, color.a) - colors = [color for i in range(len(self._data))] - self._mesh.addColors(numpy.array(colors, dtype=numpy.float32) * 0.5) + for i in range(len(self._data)): + vertices[offset + i, :] = self._data[i, :] + colors[offset + i, 0] = color.r + colors[offset + i, 1] = color.g + colors[offset + i, 2] = color.b + colors[offset + i, 3] = color.a + + self._end = self._begin + len(self._data) - 1 - indices = [] for i in range(self._begin, self._end): - indices.append(i) - indices.append(i + 1) + indices[i, 0] = i + indices[i, 1] = i + 1 - indices.append(self._end) - indices.append(self._begin) - self._mesh.addIndices(numpy.array(indices, dtype=numpy.int32)) + indices[self._end, 0] = self._end + indices[self._end, 1] = self._begin def getColor(self): if self._type == self.Inset0Type: - return [1.0, 0.0, 0.0, 1.0] + return Color(1.0, 0.0, 0.0, 1.0) elif self._type == self.InsetXType: - return [0.0, 1.0, 0.0, 1.0] + return Color(0.0, 1.0, 0.0, 1.0) elif self._type == self.SkinType: - return [1.0, 1.0, 0.0, 1.0] + return Color(1.0, 1.0, 0.0, 1.0) elif self._type == self.SupportType: - return [0.0, 1.0, 1.0, 1.0] + return Color(0.0, 1.0, 1.0, 1.0) elif self._type == self.SkirtType: - return [0.0, 1.0, 1.0, 1.0] + return Color(0.0, 1.0, 1.0, 1.0) elif self._type == self.InfillType: - return [1.0, 1.0, 0.0, 1.0] + return Color(1.0, 1.0, 0.0, 1.0) elif self._type == self.SupportInfillType: - return [0.0, 1.0, 1.0, 1.0] + return Color(0.0, 1.0, 1.0, 1.0) else: - return [1.0, 1.0, 1.0, 1.0] + return Color(1.0, 1.0, 1.0, 1.0) + + def vertexCount(self): + return len(self._data) @property def type(self): diff --git a/plugins/CuraEngineBackend/ProcessSlicedObjectListJob.py b/plugins/CuraEngineBackend/ProcessSlicedObjectListJob.py index 6113da78a0..4c6e0fd2ea 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedObjectListJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedObjectListJob.py @@ -7,18 +7,30 @@ 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 . import LayerData import numpy import struct +catalog = i18nCatalog("cura") + class ProcessSlicedObjectListJob(Job): def __init__(self, message): super().__init__() self._message = message self._scene = Application.getInstance().getController().getScene() + self._progress = None + Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) + def run(self): + if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": + self._progress = Message(catalog.i18nc("Layers View mode", "Layers"), 0, False, 0) + self._progress.show() + objectIdMap = {} new_node = SceneNode() ## Put all nodes in a dict identified by ID @@ -32,6 +44,15 @@ class ProcessSlicedObjectListJob(Job): settings = Application.getInstance().getActiveMachine() layerHeight = settings.getSettingValueByKey("layer_height") + center = None + if not settings.getSettingValueByKey("machine_center_is_zero"): + center = numpy.array([settings.getSettingValueByKey("machine_width") / 2, 0.0, -settings.getSettingValueByKey("machine_depth") / 2]) + else: + center = numpy.array([0.0, 0.0, 0.0]) + + if self._progress: + self._progress.setProgress(2) + mesh = MeshData() for object in self._message.objects: try: @@ -53,15 +74,37 @@ class ProcessSlicedObjectListJob(Job): points[:,2] *= -1 - if not settings.getSettingValueByKey("machine_center_is_zero"): - center = [settings.getSettingValueByKey("machine_width") / 2, 0.0, -settings.getSettingValueByKey("machine_depth") / 2] - points -= numpy.array(center) + points -= numpy.array(center) layerData.addPolygon(layer.id, polygon.type, points, polygon.line_width) + if self._progress: + self._progress.setProgress(50) + # We are done processing all the layers we got from the engine, now create a mesh out of the data layerData.build() mesh.layerData = layerData + if self._progress: + self._progress.setProgress(100) + new_node.setMeshData(mesh) new_node.setParent(self._scene.getRoot()) + + view = Application.getInstance().getController().getActiveView() + if view.getPluginId() == "LayerView": + view.resetLayerData() + + if self._progress: + self._progress.hide() + + def _onActiveViewChanged(self): + if self.isRunning(): + if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": + if not self._progress: + self._progress = Message(catalog.i18nc("Layers View mode", "Layers"), 0, False, 0) + self._progress.show() + else: + if self._progress: + self._progress.hide() + diff --git a/plugins/LayerView/LayerView.py b/plugins/LayerView/LayerView.py index 17cea9988c..617dda411a 100644 --- a/plugins/LayerView/LayerView.py +++ b/plugins/LayerView/LayerView.py @@ -39,6 +39,9 @@ class LayerView(View): def getMaxLayers(self): return self._max_layers + def resetLayerData(self): + self._current_layer_mesh = None + def beginRendering(self): scene = self.getController().getScene() renderer = self.getRenderer() diff --git a/plugins/USBPrinting/PrinterConnection.py b/plugins/USBPrinting/PrinterConnection.py index 4a9a73bb7b..2ac8dbab0f 100644 --- a/plugins/USBPrinting/PrinterConnection.py +++ b/plugins/USBPrinting/PrinterConnection.py @@ -173,6 +173,10 @@ class PrinterConnection(SignalEmitter): 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 not self._serial or not programmer.serial: + self._is_connecting = False + return # If the programmer connected, we know its an atmega based version. Not all that usefull, but it does give some debugging information. for baud_rate in self._getBaudrateList(): # Cycle all baud rates (auto detect) diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 830470c9c6..9756b3550f 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -37,9 +37,11 @@ UM.MainWindow { Instantiator { model: Printer.recentFiles MenuItem { - property url filePath: modelData; - text: (index + 1) + ". " + modelData.slice(modelData.lastIndexOf("/") + 1); - onTriggered: UM.MeshFileHandler.readLocalFile(filePath); + text: { + var path = modelData.toString() + return (index + 1) + ". " + path.slice(path.lastIndexOf("/") + 1); + } + onTriggered: UM.MeshFileHandler.readLocalFile(modelData); } onObjectAdded: fileMenu.insertItem(index, object) onObjectRemoved: fileMenu.removeItem(object) @@ -281,8 +283,8 @@ UM.MainWindow { } Rectangle { - x: base.mouseX; - y: base.mouseY; + x: base.mouseX + UM.Theme.sizes.default_margin.width; + y: base.mouseY + UM.Theme.sizes.default_margin.height; width: childrenRect.width; height: childrenRect.height;