Merge branch '15.06'

* 15.06:
  Update changelog
  Correct the bottom offset we add when setting the volume for scale to max
  Display progress information during processing of layer data
  If findObject returns none but object_id != 0 use the selected object
  Offset the displayed rotation angle so it does not overlap the mouse cursor
  Abort attempts to connect if an error is thrown when connecting to the serial port
  Fix recent files on Windows
  Defer opening the webbrowser until the next run of the event loop
  Disable slicing and platform physics when an operation is being performed
  Rework LayerData mesh generation for improved performance
  Performance: Only calculate the platform center once, not for every poly
  Add application icons for all three platforms
This commit is contained in:
Arjen Hiemstra 2015-06-24 12:06:39 +02:00
commit a429e362ad
16 changed files with 190 additions and 51 deletions

26
CHANGES
View file

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

View file

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

View file

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

View file

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

BIN
icons/cura-128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
icons/cura-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 B

BIN
icons/cura-48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

BIN
icons/cura-64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
icons/cura.icns Normal file

Binary file not shown.

BIN
icons/cura.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

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

View file

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

View file

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

View file

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

View file

@ -174,6 +174,10 @@ class PrinterConnection(SignalEmitter):
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)

View file

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