mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-24 15:13:56 -06:00
Merge branch '15.06' of https://github.com/Ultimaker/Cura into 15.06
Someone else pushed changes before I did
This commit is contained in:
commit
1acd3ee2a5
16 changed files with 190 additions and 51 deletions
26
CHANGES
26
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.
|
||||
|
|
|
@ -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)
|
|
@ -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()
|
||||
|
|
|
@ -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
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
BIN
icons/cura-32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 625 B |
BIN
icons/cura-48.png
Normal file
BIN
icons/cura-48.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1 KiB |
BIN
icons/cura-64.png
Normal file
BIN
icons/cura-64.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
icons/cura.icns
Normal file
BIN
icons/cura.icns
Normal file
Binary file not shown.
BIN
icons/cura.ico
Normal file
BIN
icons/cura.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue