mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-05-28 17:18:39 +08:00
Merge branch 'master' into feature_mold
This commit is contained in:
commit
a4272bf7be
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,6 +10,7 @@ resources/i18n/x-test
|
||||
resources/firmware
|
||||
resources/materials
|
||||
LC_MESSAGES
|
||||
.cache
|
||||
|
||||
# Editors and IDEs.
|
||||
*kdev*
|
||||
|
@ -1,14 +1,16 @@
|
||||
project(cura NONE)
|
||||
cmake_minimum_required(VERSION 2.8.12)
|
||||
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/
|
||||
${CMAKE_MODULE_PATH})
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
set(URANIUM_SCRIPTS_DIR "${CMAKE_SOURCE_DIR}/../uranium/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository")
|
||||
set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE DIRECTORY "The location of the Uranium repository")
|
||||
set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository")
|
||||
|
||||
# Tests
|
||||
# Note that we use exit 0 here to not mark the build as a failure on test failure
|
||||
add_custom_target(tests)
|
||||
add_custom_command(TARGET tests POST_BUILD COMMAND "PYTHONPATH=${CMAKE_SOURCE_DIR}/../Uranium/:${CMAKE_SOURCE_DIR}" ${PYTHON_EXECUTABLE} -m pytest -r a --junitxml=${CMAKE_BINARY_DIR}/junit.xml ${CMAKE_SOURCE_DIR} || exit 0)
|
||||
include(CuraTests)
|
||||
|
||||
option(CURA_DEBUGMODE "Enable debug dialog and other debug features" OFF)
|
||||
if(CURA_DEBUGMODE)
|
||||
@ -21,6 +23,7 @@ configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desk
|
||||
configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY)
|
||||
|
||||
if(NOT ${URANIUM_SCRIPTS_DIR} STREQUAL "")
|
||||
list(APPEND CMAKE_MODULE_PATH ${URANIUM_DIR}/cmake)
|
||||
include(UraniumTranslationTools)
|
||||
# Extract Strings
|
||||
add_custom_target(extract-messages ${URANIUM_SCRIPTS_DIR}/extract-messages ${CMAKE_SOURCE_DIR} cura)
|
||||
|
40
Jenkinsfile
vendored
Normal file
40
Jenkinsfile
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
parallel_nodes(['linux && cura', 'windows && cura']) {
|
||||
// Prepare building
|
||||
stage('Prepare') {
|
||||
// Ensure we start with a clean build directory.
|
||||
step([$class: 'WsCleanup'])
|
||||
|
||||
// Checkout whatever sources are linked to this pipeline.
|
||||
checkout scm
|
||||
}
|
||||
|
||||
// If any error occurs during building, we want to catch it and continue with the "finale" stage.
|
||||
catchError {
|
||||
// Building and testing should happen in a subdirectory.
|
||||
dir('build') {
|
||||
// Perform the "build". Since Uranium is Python code, this basically only ensures CMake is setup.
|
||||
stage('Build') {
|
||||
// Ensure CMake is setup. Note that since this is Python code we do not really "build" it.
|
||||
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/master")
|
||||
cmake("..", "-DCMAKE_PREFIX_PATH=${env.CURA_ENVIRONMENT_PATH} -DCMAKE_BUILD_TYPE=Release -DURANIUM_DIR=${uranium_dir}")
|
||||
}
|
||||
|
||||
// Try and run the unit tests. If this stage fails, we consider the build to be "unstable".
|
||||
stage('Unit Test') {
|
||||
try {
|
||||
make('test')
|
||||
} catch(e) {
|
||||
currentBuild.result = "UNSTABLE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Perform any post-build actions like notification and publishing of unit tests.
|
||||
stage('Finalize') {
|
||||
// Publish the test results to Jenkins.
|
||||
junit allowEmptyResults: true, testResults: 'build/junit*.xml'
|
||||
|
||||
notify_build_result(env.CURA_EMAIL_RECIPIENTS, '#cura-dev', ['master', '2.'])
|
||||
}
|
||||
}
|
48
cmake/CuraTests.cmake
Normal file
48
cmake/CuraTests.cmake
Normal file
@ -0,0 +1,48 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
enable_testing()
|
||||
include(CMakeParseArguments)
|
||||
|
||||
find_package(PythonInterp 3.5.0 REQUIRED)
|
||||
|
||||
function(cura_add_test)
|
||||
set(_single_args NAME DIRECTORY PYTHONPATH)
|
||||
cmake_parse_arguments("" "" "${_single_args}" "" ${ARGN})
|
||||
|
||||
if(NOT _NAME)
|
||||
message(FATAL_ERROR "cura_add_test requires a test name argument")
|
||||
endif()
|
||||
|
||||
if(NOT _DIRECTORY)
|
||||
message(FATAL_ERROR "cura_add_test requires a directory to test")
|
||||
endif()
|
||||
|
||||
if(NOT _PYTHONPATH)
|
||||
set(_PYTHONPATH ${_DIRECTORY})
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
string(REPLACE "|" "\\;" _PYTHONPATH ${_PYTHONPATH})
|
||||
else()
|
||||
string(REPLACE "|" ":" _PYTHONPATH ${_PYTHONPATH})
|
||||
endif()
|
||||
|
||||
add_test(
|
||||
NAME ${_NAME}
|
||||
COMMAND ${PYTHON_EXECUTABLE} -m pytest --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY}
|
||||
)
|
||||
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT LANG=C)
|
||||
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}")
|
||||
endfunction()
|
||||
|
||||
cura_add_test(NAME pytest-main DIRECTORY ${CMAKE_SOURCE_DIR}/tests PYTHONPATH "${CMAKE_SOURCE_DIR}|${URANIUM_DIR}")
|
||||
|
||||
file(GLOB_RECURSE _plugins plugins/*/__init__.py)
|
||||
foreach(_plugin ${_plugins})
|
||||
get_filename_component(_plugin_directory ${_plugin} DIRECTORY)
|
||||
if(EXISTS ${_plugin_directory}/tests)
|
||||
get_filename_component(_plugin_name ${_plugin_directory} NAME)
|
||||
cura_add_test(NAME pytest-${_plugin_name} DIRECTORY ${_plugin_directory} PYTHONPATH "${_plugin_directory}|${CMAKE_SOURCE_DIR}|${URANIUM_DIR}")
|
||||
endif()
|
||||
endforeach()
|
@ -629,11 +629,12 @@ class BuildVolume(SceneNode):
|
||||
|
||||
if not self._global_container_stack.getProperty("machine_center_is_zero", "value"):
|
||||
prime_x = prime_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
|
||||
prime_y = prime_x + machine_depth / 2
|
||||
prime_y = prime_y + machine_depth / 2
|
||||
|
||||
prime_polygon = Polygon.approximatedCircle(PRIME_CLEARANCE)
|
||||
prime_polygon = prime_polygon.translate(prime_x, prime_y)
|
||||
prime_polygon = prime_polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size))
|
||||
|
||||
prime_polygon = prime_polygon.translate(prime_x, prime_y)
|
||||
result[extruder.getId()] = [prime_polygon]
|
||||
|
||||
return result
|
||||
|
@ -24,7 +24,7 @@ class ConvexHullNode(SceneNode):
|
||||
self._original_parent = parent
|
||||
|
||||
# Color of the drawn convex hull
|
||||
self._color = None
|
||||
self._color = Color(*Application.getInstance().getTheme().getColor("convex_hull").getRgb())
|
||||
|
||||
# The y-coordinate of the convex hull mesh. Must not be 0, to prevent z-fighting.
|
||||
self._mesh_height = 0.1
|
||||
@ -73,8 +73,6 @@ class ConvexHullNode(SceneNode):
|
||||
return True
|
||||
|
||||
def _onNodeDecoratorsChanged(self, node):
|
||||
self._color = Color(*Application.getInstance().getTheme().getColor("convex_hull").getRgb())
|
||||
|
||||
convex_hull_head = self._node.callDecoration("getConvexHullHead")
|
||||
if convex_hull_head:
|
||||
convex_hull_head_builder = MeshBuilder()
|
||||
|
@ -4,6 +4,7 @@ from PyQt5.QtNetwork import QLocalServer
|
||||
from PyQt5.QtNetwork import QLocalSocket
|
||||
|
||||
from UM.Qt.QtApplication import QtApplication
|
||||
from UM.FileHandler.ReadFileJob import ReadFileJob
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Camera import Camera
|
||||
from UM.Math.Vector import Vector
|
||||
@ -25,6 +26,8 @@ from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Settings.Validator import Validator
|
||||
from UM.Message import Message
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Workspace.WorkspaceReader import WorkspaceReader
|
||||
from UM.Platform import Platform
|
||||
|
||||
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
||||
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
||||
@ -199,7 +202,7 @@ class CuraApplication(QtApplication):
|
||||
|
||||
self._message_box_callback = None
|
||||
self._message_box_callback_arguments = []
|
||||
|
||||
self._preferred_mimetype = ""
|
||||
self._i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity)
|
||||
@ -224,7 +227,7 @@ class CuraApplication(QtApplication):
|
||||
ContainerRegistry.getInstance().addContainer(empty_material_container)
|
||||
empty_quality_container = copy.deepcopy(empty_container)
|
||||
empty_quality_container._id = "empty_quality"
|
||||
empty_quality_container.setName("Not supported")
|
||||
empty_quality_container.setName("Not Supported")
|
||||
empty_quality_container.addMetaDataEntry("quality_type", "normal")
|
||||
empty_quality_container.addMetaDataEntry("type", "quality")
|
||||
ContainerRegistry.getInstance().addContainer(empty_quality_container)
|
||||
@ -245,11 +248,14 @@ class CuraApplication(QtApplication):
|
||||
Preferences.getInstance().addPreference("mesh/scale_tiny_meshes", True)
|
||||
Preferences.getInstance().addPreference("cura/dialog_on_project_save", True)
|
||||
Preferences.getInstance().addPreference("cura/asked_dialog_on_project_save", False)
|
||||
Preferences.getInstance().addPreference("cura/choice_on_profile_override", 0)
|
||||
Preferences.getInstance().addPreference("cura/choice_on_profile_override", "always_ask")
|
||||
Preferences.getInstance().addPreference("cura/choice_on_open_project", "always_ask")
|
||||
|
||||
Preferences.getInstance().addPreference("cura/currency", "€")
|
||||
Preferences.getInstance().addPreference("cura/material_settings", "{}")
|
||||
|
||||
Preferences.getInstance().addPreference("view/invert_zoom", False)
|
||||
|
||||
for key in [
|
||||
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
|
||||
"dialog_profile_path",
|
||||
@ -314,6 +320,9 @@ class CuraApplication(QtApplication):
|
||||
|
||||
self.applicationShuttingDown.connect(self.saveSettings)
|
||||
self.engineCreatedSignal.connect(self._onEngineCreated)
|
||||
|
||||
self.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||
self._onGlobalContainerChanged()
|
||||
self._recent_files = []
|
||||
files = Preferences.getInstance().getValue("cura/recent_files").split(";")
|
||||
for f in files:
|
||||
@ -338,10 +347,10 @@ class CuraApplication(QtApplication):
|
||||
|
||||
def discardOrKeepProfileChanges(self):
|
||||
choice = Preferences.getInstance().getValue("cura/choice_on_profile_override")
|
||||
if choice == 1:
|
||||
if choice == "always_discard":
|
||||
# don't show dialog and DISCARD the profile
|
||||
self.discardOrKeepProfileChangesClosed("discard")
|
||||
elif choice == 2:
|
||||
elif choice == "always_keep":
|
||||
# don't show dialog and KEEP the profile
|
||||
self.discardOrKeepProfileChangesClosed("keep")
|
||||
else:
|
||||
@ -717,7 +726,9 @@ class CuraApplication(QtApplication):
|
||||
else:
|
||||
# Default
|
||||
self.getController().setActiveTool("TranslateTool")
|
||||
if Preferences.getInstance().getValue("view/center_on_select"):
|
||||
|
||||
# Hack: QVector bindings are broken on PyQt 5.7.1 on Windows. This disables it being called at all.
|
||||
if Preferences.getInstance().getValue("view/center_on_select") and not Platform.isWindows():
|
||||
self._center_after_select = True
|
||||
else:
|
||||
if self.getController().getActiveTool():
|
||||
@ -731,14 +742,30 @@ class CuraApplication(QtApplication):
|
||||
self._camera_animation.setTarget(Selection.getSelectedObject(0).getWorldPosition())
|
||||
self._camera_animation.start()
|
||||
|
||||
def _onGlobalContainerChanged(self):
|
||||
if self._global_container_stack is not None:
|
||||
machine_file_formats = [file_type.strip() for file_type in self._global_container_stack.getMetaDataEntry("file_formats").split(";")]
|
||||
new_preferred_mimetype = ""
|
||||
if machine_file_formats:
|
||||
new_preferred_mimetype = machine_file_formats[0]
|
||||
|
||||
if new_preferred_mimetype != self._preferred_mimetype:
|
||||
self._preferred_mimetype = new_preferred_mimetype
|
||||
self.preferredOutputMimetypeChanged.emit()
|
||||
|
||||
requestAddPrinter = pyqtSignal()
|
||||
activityChanged = pyqtSignal()
|
||||
sceneBoundingBoxChanged = pyqtSignal()
|
||||
preferredOutputMimetypeChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(bool, notify = activityChanged)
|
||||
def platformActivity(self):
|
||||
return self._platform_activity
|
||||
|
||||
@pyqtProperty(str, notify=preferredOutputMimetypeChanged)
|
||||
def preferredOutputMimetype(self):
|
||||
return self._preferred_mimetype
|
||||
|
||||
@pyqtProperty(str, notify = sceneBoundingBoxChanged)
|
||||
def getSceneBoundingBoxString(self):
|
||||
return self._i18n_catalog.i18nc("@info", "%(width).1f x %(depth).1f x %(height).1f mm") % {'width' : self._scene_bounding_box.width.item(), 'depth': self._scene_bounding_box.depth.item(), 'height' : self._scene_bounding_box.height.item()}
|
||||
@ -1031,7 +1058,9 @@ class CuraApplication(QtApplication):
|
||||
transformation.setTranslation(zero_translation)
|
||||
transformed_mesh = mesh.getTransformed(transformation)
|
||||
center = transformed_mesh.getCenterPosition()
|
||||
object_centers.append(center)
|
||||
if center is not None:
|
||||
object_centers.append(center)
|
||||
|
||||
if object_centers and len(object_centers) > 0:
|
||||
middle_x = sum([v.x for v in object_centers]) / len(object_centers)
|
||||
middle_y = sum([v.y for v in object_centers]) / len(object_centers)
|
||||
@ -1102,7 +1131,7 @@ class CuraApplication(QtApplication):
|
||||
fileLoaded = pyqtSignal(str)
|
||||
|
||||
def _onJobFinished(self, job):
|
||||
if type(job) is not ReadMeshJob or not job.getResult():
|
||||
if (not isinstance(job, ReadMeshJob) and not isinstance(job, ReadFileJob)) or not job.getResult():
|
||||
return
|
||||
|
||||
f = QUrl.fromLocalFile(job.getFileName())
|
||||
@ -1241,3 +1270,16 @@ class CuraApplication(QtApplication):
|
||||
|
||||
def addNonSliceableExtension(self, extension):
|
||||
self._non_sliceable_extensions.append(extension)
|
||||
|
||||
@pyqtSlot(str, result=bool)
|
||||
def checkIsValidProjectFile(self, file_url):
|
||||
"""
|
||||
Checks if the given file URL is a valid project file.
|
||||
"""
|
||||
file_path = QUrl(file_url).toLocalFile()
|
||||
workspace_reader = self.getWorkspaceFileHandler().getReaderForFile(file_path)
|
||||
if workspace_reader is None:
|
||||
return False # non-project files won't get a reader
|
||||
|
||||
result = workspace_reader.preRead(file_path, show_dialog=False)
|
||||
return result == WorkspaceReader.PreReadResult.accepted
|
||||
|
@ -1,10 +1,8 @@
|
||||
from .LayerPolygon import LayerPolygon
|
||||
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
|
||||
import numpy
|
||||
|
||||
|
||||
class Layer:
|
||||
def __init__(self, layer_id):
|
||||
self._id = layer_id
|
||||
@ -80,8 +78,7 @@ class Layer:
|
||||
else:
|
||||
for polygon in self._polygons:
|
||||
line_count += polygon.jumpCount
|
||||
|
||||
|
||||
|
||||
# Reserve the neccesary space for the data upfront
|
||||
builder.reserveFaceAndVertexCount(2 * line_count, 4 * line_count)
|
||||
|
||||
@ -94,7 +91,7 @@ class Layer:
|
||||
# Line types of the points we want to draw
|
||||
line_types = polygon.types[index_mask]
|
||||
|
||||
# Shift the z-axis according to previous implementation.
|
||||
# Shift the z-axis according to previous implementation.
|
||||
if make_mesh:
|
||||
points[polygon.isInfillOrSkinType(line_types), 1::3] -= 0.01
|
||||
else:
|
||||
@ -106,13 +103,14 @@ class Layer:
|
||||
# Scale all normals by the line width of the current line so we can easily offset.
|
||||
normals *= (polygon.lineWidths[index_mask.ravel()] / 2)
|
||||
|
||||
# Create 4 points to draw each line segment, points +- normals results in 2 points each. Reshape to one point per line
|
||||
# Create 4 points to draw each line segment, points +- normals results in 2 points each.
|
||||
# After this we reshape to one point per line.
|
||||
f_points = numpy.concatenate((points-normals, points+normals), 1).reshape((-1, 3))
|
||||
# __index_pattern defines which points to use to draw the two faces for each lines egment, the following linesegment is offset by 4
|
||||
|
||||
# __index_pattern defines which points to use to draw the two faces for each lines egment, the following linesegment is offset by 4
|
||||
f_indices = ( self.__index_pattern + numpy.arange(0, 4 * len(normals), 4, dtype=numpy.int32).reshape((-1, 1)) ).reshape((-1, 3))
|
||||
f_colors = numpy.repeat(polygon.mapLineTypeToColor(line_types), 4, 0)
|
||||
|
||||
builder.addFacesWithColor(f_points, f_indices, f_colors)
|
||||
|
||||
|
||||
return builder.build()
|
@ -2,11 +2,12 @@
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
from UM.Mesh.MeshData import MeshData
|
||||
|
||||
|
||||
## Class to holds the layer mesh and information about the layers.
|
||||
# Immutable, use LayerDataBuilder to create one of these.
|
||||
class LayerData(MeshData):
|
||||
def __init__(self, vertices = None, normals = None, indices = None, colors = None, uvs = None, file_name = None,
|
||||
center_position = None, layers=None, element_counts=None, attributes=None):
|
||||
center_position = None, layers=None, element_counts=None, attributes=None):
|
||||
super().__init__(vertices=vertices, normals=normals, indices=indices, colors=colors, uvs=uvs,
|
||||
file_name=file_name, center_position=center_position, attributes=attributes)
|
||||
self._layers = layers
|
||||
|
@ -8,6 +8,7 @@ from .LayerData import LayerData
|
||||
|
||||
import numpy
|
||||
|
||||
|
||||
## Builder class for constructing a LayerData object
|
||||
class LayerDataBuilder(MeshBuilder):
|
||||
def __init__(self):
|
||||
|
@ -171,6 +171,10 @@ class PlatformPhysics:
|
||||
self._enabled = False
|
||||
|
||||
def _onToolOperationStopped(self, tool):
|
||||
# Selection tool should not trigger an update.
|
||||
if tool.getPluginId() == "SelectionTool":
|
||||
return
|
||||
|
||||
if tool.getPluginId() == "TranslateTool":
|
||||
for node in Selection.getAllSelectedObjects():
|
||||
if node.getBoundingBox().bottom < 0:
|
||||
|
@ -5,6 +5,7 @@ from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
from UM.Qt.Duration import Duration
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
@ -109,13 +110,21 @@ class PrintInformation(QObject):
|
||||
return self._material_costs
|
||||
|
||||
def _onPrintDurationMessage(self, total_time, material_amounts):
|
||||
self._current_print_time.setDuration(total_time)
|
||||
if total_time != total_time: # Check for NaN. Engine can sometimes give us weird values.
|
||||
Logger.log("w", "Received NaN for print duration message")
|
||||
self._current_print_time.setDuration(0)
|
||||
else:
|
||||
self._current_print_time.setDuration(total_time)
|
||||
|
||||
self.currentPrintTimeChanged.emit()
|
||||
|
||||
self._material_amounts = material_amounts
|
||||
self._calculateInformation()
|
||||
|
||||
def _calculateInformation(self):
|
||||
if Application.getInstance().getGlobalContainerStack() is None:
|
||||
return
|
||||
|
||||
# Material amount is sent as an amount of mm^3, so calculate length from that
|
||||
r = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value") / 2
|
||||
self._material_lengths = []
|
||||
|
@ -813,6 +813,10 @@ class MachineManager(QObject):
|
||||
Logger.log("e", "Tried to set quality to a container that is not of the right type")
|
||||
return
|
||||
|
||||
# Check if it was at all possible to find new settings
|
||||
if new_quality_settings_list is None:
|
||||
return
|
||||
|
||||
name_changed_connect_stacks = [] # Connect these stacks to the name changed callback
|
||||
for setting_info in new_quality_settings_list:
|
||||
stack = setting_info["stack"]
|
||||
@ -889,7 +893,12 @@ class MachineManager(QObject):
|
||||
quality_changes_profiles = quality_manager.findQualityChangesByName(quality_changes_name,
|
||||
global_machine_definition)
|
||||
|
||||
global_quality_changes = [qcp for qcp in quality_changes_profiles if qcp.getMetaDataEntry("extruder") is None][0]
|
||||
global_quality_changes = [qcp for qcp in quality_changes_profiles if qcp.getMetaDataEntry("extruder") is None]
|
||||
if global_quality_changes:
|
||||
global_quality_changes = global_quality_changes[0]
|
||||
else:
|
||||
Logger.log("e", "Could not find the global quality changes container with name %s", quality_changes_name)
|
||||
return None
|
||||
material = global_container_stack.findContainer(type="material")
|
||||
|
||||
# For the global stack, find a quality which matches the quality_type in
|
||||
|
@ -50,7 +50,6 @@ sys.excepthook = exceptHook
|
||||
# first seems to prevent Sip from going into a state where it
|
||||
# tries to create PyQt objects on a non-main thread.
|
||||
import Arcus #@UnusedImport
|
||||
from UM.Platform import Platform
|
||||
import cura.CuraApplication
|
||||
import cura.Settings.CuraContainerRegistry
|
||||
|
||||
|
6
plugins/3MFReader/ThreeMFReader.py
Normal file → Executable file
6
plugins/3MFReader/ThreeMFReader.py
Normal file → Executable file
@ -16,6 +16,7 @@ from UM.Application import Application
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from cura.QualityManager import QualityManager
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from cura.SliceableObjectDecorator import SliceableObjectDecorator
|
||||
|
||||
MYPY = False
|
||||
|
||||
@ -144,6 +145,11 @@ class ThreeMFReader(MeshReader):
|
||||
group_decorator = GroupDecorator()
|
||||
um_node.addDecorator(group_decorator)
|
||||
um_node.setSelectable(True)
|
||||
if um_node.getMeshData():
|
||||
# Assuming that all nodes with mesh data are printable objects
|
||||
# affects (auto) slicing
|
||||
sliceable_decorator = SliceableObjectDecorator()
|
||||
um_node.addDecorator(sliceable_decorator)
|
||||
return um_node
|
||||
|
||||
def read(self, file_name):
|
||||
|
@ -47,7 +47,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
self._id_mapping[old_id] = self._container_registry.uniqueName(old_id)
|
||||
return self._id_mapping[old_id]
|
||||
|
||||
def preRead(self, file_name):
|
||||
def preRead(self, file_name, show_dialog=True, *args, **kwargs):
|
||||
self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name)
|
||||
if self._3mf_mesh_reader and self._3mf_mesh_reader.preRead(file_name) == WorkspaceReader.PreReadResult.accepted:
|
||||
pass
|
||||
@ -167,6 +167,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
Logger.log("w", "File %s is not a valid workspace.", file_name)
|
||||
return WorkspaceReader.PreReadResult.failed
|
||||
|
||||
# In case we use preRead() to check if a file is a valid project file, we don't want to show a dialog.
|
||||
if not show_dialog:
|
||||
return WorkspaceReader.PreReadResult.accepted
|
||||
|
||||
# Show the dialog, informing the user what is about to happen.
|
||||
self._dialog.setMachineConflict(machine_conflict)
|
||||
self._dialog.setQualityChangesConflict(quality_changes_conflict)
|
||||
|
@ -1,9 +1,54 @@
|
||||
[2.5.0]
|
||||
*Layerview double slider.
|
||||
The layerview now has a nice slider with double handles where you can drag maximum layer, minimum layer and the layer range. Thansk to community member Aldo Hoeben for this feature.
|
||||
*Improved speed
|
||||
We’ve made changing printers, profiles, materials, and print cores even faster. 3MF processing is also much faster now. Opening a 3MF file now takes one tenth of the time.
|
||||
|
||||
*Included PauseBackendPlugin.
|
||||
This enables pausing the backend and manually start the backend. Thanks to community member Aldo Hoeben for this feature.
|
||||
*Speedup engine – Multithreading
|
||||
Cura can process multiple operations at the same time during slicing. Supported by Windows and Linux operating systems only.
|
||||
|
||||
*Preheat the build plate (with a connected printer)
|
||||
Users can now set the Ultimaker 3 to preheat the build plate, which reduces the downtime, allowing to manually speed up the printing workflow.
|
||||
|
||||
*Better layout for 3D layer view options
|
||||
An improved layer view has been implemented for computers that support OpenGL 4.1. For OpenGL 2.0 to 4.0, we will automatically switch to the old layer view.
|
||||
|
||||
*Disable automatic slicing
|
||||
An option to disable auto-slicing has been added for the better user experience.
|
||||
|
||||
*Auto-scale off by default
|
||||
This change speaks for itself.
|
||||
|
||||
*Print cost calculation
|
||||
The latest version of Cura now contains code to help users calculate the cost of their prints. To do so, users need to enter a cost per spool and an amount of materials per spool. It is also possible to set the cost per material and gain better control of the expenses. Thanks to our community member Aldo Hoeben for adding this feature.
|
||||
|
||||
*G-code reader
|
||||
The g-code reader has been reintroduced, which means users can load g-code from file and display it in layer view. Users can also print saved g-code files with Cura, share and re-use them, as well as preview the printed object via the g-code viewer. Thanks to AlephObjects for this feature.
|
||||
|
||||
*Discard or Keep Changes popup
|
||||
We’ve changed the popup that appears when a user changes a printing profile after setting custom printing settings. It is now more informative and helpful.
|
||||
|
||||
*Bug fixes
|
||||
- Window overflow: On some configurations (OS and screen dependant), an overflow on the General (Preferences) panel and the credits list on the About window occurred. This is now fixed.
|
||||
- “Center camera when the item is selected”: This is now set to ‘off’ by default.
|
||||
- Removal of file extension: When users save a file or project (without changing the file type), no file extension is added to the name. It’s only when users change to another file type that the extension is added.
|
||||
- Ultimaker 3 Extended connectivity. Selecting Ultimaker 3 Extended in Cura let you connect and print with Ultimaker 3, without any warning. This now has been fixed.
|
||||
- Different Y / Z colors: Y and Z colors in the tool menu are now similar to the colors on the build plate.
|
||||
- No collision areas: No collision areas used to be generated for some models when "keep models apart" was activated. This is now fixed.
|
||||
- Perimeter gaps: Perimeter gaps are not filled often enough; we’ve now amended this.
|
||||
- File location after restart: The old version of Cura didn’t remember the last opened file location after it’s been restarted. Now it has been fixed.
|
||||
- Project name: The project name changes after the project is opened. This now has been fixed.
|
||||
- Slicing when error value is given (print core 2): When a support is printed with the Extruder 2 (PVA), some support settings will trigger a slice when an error value is given. We’ve now sorted this out.
|
||||
- Support Towers: Support Towers can now be disabled.
|
||||
- Support bottoms: When putting one object on top of another with some space in between, and selecting support with support bottom interface, no support bottom is printed. This has now been resolved.
|
||||
- Summary box size: We’ve enlarged the summary box when saving the project.
|
||||
- Cubic subdivision infill: In the past, the cubic subdivision infill sometimes didn’t produce the infill (WIN) – this has now been addressed.
|
||||
- Spiralize outer contour and fill small gaps: When combining Fill Gaps Between Walls with Spiralize Outer Contour, the model gets a massive infill.
|
||||
- Experimental post-processing plugin: Since the TweakAtZ post-processing plugin is not officially supported, we added the ‘Experimental’ tag.
|
||||
|
||||
*3rd party printers (bug fixes)
|
||||
- Folgertech printer definition has been added.
|
||||
- Hello BEE Prusa printer definition has been added.
|
||||
- Velleman Vertex K8400 printer definitions have been added for both single-extrusion and dual-extrusion versions.
|
||||
- Material profiles for Cartesio printers have been updated.
|
||||
|
||||
[2.4.0]
|
||||
*Project saving & opening
|
||||
|
188
plugins/GCodeReader/GCodeReader.py
Normal file → Executable file
188
plugins/GCodeReader/GCodeReader.py
Normal file → Executable file
@ -2,6 +2,7 @@
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||
from UM.Math.Vector import Vector
|
||||
@ -9,6 +10,7 @@ from UM.Mesh.MeshReader import MeshReader
|
||||
from UM.Message import Message
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Preferences import Preferences
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
@ -17,6 +19,7 @@ from cura import LayerDataBuilder
|
||||
from cura import LayerDataDecorator
|
||||
from cura.LayerPolygon import LayerPolygon
|
||||
from cura.GCodeListDecorator import GCodeListDecorator
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
import numpy
|
||||
import math
|
||||
@ -32,14 +35,22 @@ class GCodeReader(MeshReader):
|
||||
Application.getInstance().hideMessageSignal.connect(self._onHideMessage)
|
||||
self._cancelled = False
|
||||
self._message = None
|
||||
self._layer_number = 0
|
||||
self._extruder_number = 0
|
||||
self._layer_type = LayerPolygon.Inset0Type
|
||||
self._clearValues()
|
||||
self._scene_node = None
|
||||
self._position = namedtuple('Position', ['x', 'y', 'z', 'e'])
|
||||
self._is_layers_in_file = False # Does the Gcode have the layers comment?
|
||||
self._extruder_offsets = {} # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
|
||||
self._current_layer_thickness = 0.2 # default
|
||||
|
||||
Preferences.getInstance().addPreference("gcodereader/show_caution", True)
|
||||
|
||||
def _clearValues(self):
|
||||
self._extruder = 0
|
||||
self._extruder_number = 0
|
||||
self._layer_type = LayerPolygon.Inset0Type
|
||||
self._layer = 0
|
||||
self._layer_number = 0
|
||||
self._previous_z = 0
|
||||
self._layer_data_builder = LayerDataBuilder.LayerDataBuilder()
|
||||
self._center_is_zero = False
|
||||
@ -82,18 +93,21 @@ class GCodeReader(MeshReader):
|
||||
def _getNullBoundingBox():
|
||||
return AxisAlignedBox(minimum=Vector(0, 0, 0), maximum=Vector(10, 10, 10))
|
||||
|
||||
def _createPolygon(self, current_z, path):
|
||||
def _createPolygon(self, layer_thickness, path, extruder_offsets):
|
||||
countvalid = 0
|
||||
for point in path:
|
||||
if point[3] > 0:
|
||||
countvalid += 1
|
||||
if countvalid >= 2:
|
||||
# we know what to do now, no need to count further
|
||||
continue
|
||||
if countvalid < 2:
|
||||
return False
|
||||
try:
|
||||
self._layer_data_builder.addLayer(self._layer)
|
||||
self._layer_data_builder.setLayerHeight(self._layer, path[0][2])
|
||||
self._layer_data_builder.setLayerThickness(self._layer, math.fabs(current_z - self._previous_z))
|
||||
this_layer = self._layer_data_builder.getLayer(self._layer)
|
||||
self._layer_data_builder.addLayer(self._layer_number)
|
||||
self._layer_data_builder.setLayerHeight(self._layer_number, path[0][2])
|
||||
self._layer_data_builder.setLayerThickness(self._layer_number, layer_thickness)
|
||||
this_layer = self._layer_data_builder.getLayer(self._layer_number)
|
||||
except ValueError:
|
||||
return False
|
||||
count = len(path)
|
||||
@ -101,22 +115,19 @@ class GCodeReader(MeshReader):
|
||||
line_widths = numpy.empty((count - 1, 1), numpy.float32)
|
||||
line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
|
||||
# TODO: need to calculate actual line width based on E values
|
||||
line_widths[:, 0] = 0.4
|
||||
# TODO: need to calculate actual line heights
|
||||
line_thicknesses[:, 0] = 0.2
|
||||
line_widths[:, 0] = 0.35 # Just a guess
|
||||
line_thicknesses[:, 0] = layer_thickness
|
||||
points = numpy.empty((count, 3), numpy.float32)
|
||||
i = 0
|
||||
for point in path:
|
||||
points[i, 0] = point[0]
|
||||
points[i, 1] = point[2]
|
||||
points[i, 2] = -point[1]
|
||||
points[i, :] = [point[0] + extruder_offsets[0], point[2], -point[1] - extruder_offsets[1]]
|
||||
if i > 0:
|
||||
line_types[i - 1] = point[3]
|
||||
if point[3] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]:
|
||||
line_widths[i - 1] = 0.2
|
||||
line_widths[i - 1] = 0.1
|
||||
i += 1
|
||||
|
||||
this_poly = LayerPolygon(self._extruder, line_types, points, line_widths, line_thicknesses)
|
||||
this_poly = LayerPolygon(self._extruder_number, line_types, points, line_widths, line_thicknesses)
|
||||
this_poly.buildCache()
|
||||
|
||||
this_layer.polygons.append(this_poly)
|
||||
@ -126,30 +137,29 @@ class GCodeReader(MeshReader):
|
||||
x, y, z, e = position
|
||||
x = params.x if params.x is not None else x
|
||||
y = params.y if params.y is not None else y
|
||||
z_changed = False
|
||||
if params.z is not None:
|
||||
if z != params.z:
|
||||
z_changed = True
|
||||
self._previous_z = z
|
||||
z = params.z
|
||||
z = params.z if params.z is not None else position.z
|
||||
|
||||
if params.e is not None:
|
||||
if params.e > e[self._extruder]:
|
||||
if params.e > e[self._extruder_number]:
|
||||
path.append([x, y, z, self._layer_type]) # extrusion
|
||||
else:
|
||||
path.append([x, y, z, LayerPolygon.MoveRetractionType]) # retraction
|
||||
e[self._extruder] = params.e
|
||||
e[self._extruder_number] = params.e
|
||||
|
||||
# Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
|
||||
# Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
|
||||
if z > self._previous_z and (z - self._previous_z < 1.5):
|
||||
self._current_layer_thickness = z - self._previous_z + 0.05 # allow a tiny overlap
|
||||
self._previous_z = z
|
||||
else:
|
||||
path.append([x, y, z, LayerPolygon.MoveCombingType])
|
||||
if z_changed:
|
||||
if not self._is_layers_in_file:
|
||||
if len(path) > 1 and z > 0:
|
||||
if self._createPolygon(z, path):
|
||||
self._layer += 1
|
||||
path.clear()
|
||||
else:
|
||||
path.clear()
|
||||
|
||||
return self._position(x, y, z, e)
|
||||
|
||||
# G0 and G1 should be handled exactly the same.
|
||||
_gCode1 = _gCode0
|
||||
|
||||
## Home the head.
|
||||
def _gCode28(self, position, params, path):
|
||||
return self._position(
|
||||
params.x if params.x is not None else position.x,
|
||||
@ -157,24 +167,36 @@ class GCodeReader(MeshReader):
|
||||
0,
|
||||
position.e)
|
||||
|
||||
## Reset the current position to the values specified.
|
||||
# For example: G92 X10 will set the X to 10 without any physical motion.
|
||||
def _gCode92(self, position, params, path):
|
||||
if params.e is not None:
|
||||
position.e[self._extruder] = params.e
|
||||
position.e[self._extruder_number] = params.e
|
||||
|
||||
return self._position(
|
||||
params.x if params.x is not None else position.x,
|
||||
params.y if params.y is not None else position.y,
|
||||
params.z if params.z is not None else position.z,
|
||||
position.e)
|
||||
|
||||
_gCode1 = _gCode0
|
||||
|
||||
def _processGCode(self, G, line, position, path):
|
||||
func = getattr(self, "_gCode%s" % G, None)
|
||||
x = self._getFloat(line, "X")
|
||||
y = self._getFloat(line, "Y")
|
||||
z = self._getFloat(line, "Z")
|
||||
e = self._getFloat(line, "E")
|
||||
if func is not None:
|
||||
s = line.upper().split(" ")
|
||||
x, y, z, e = None, None, None, None
|
||||
for item in s[1:]:
|
||||
if len(item) <= 1:
|
||||
continue
|
||||
if item.startswith(";"):
|
||||
continue
|
||||
if item[0] == "X":
|
||||
x = float(item[1:])
|
||||
if item[0] == "Y":
|
||||
y = float(item[1:])
|
||||
if item[0] == "Z":
|
||||
z = float(item[1:])
|
||||
if item[0] == "E":
|
||||
e = float(item[1:])
|
||||
if (x is not None and x < 0) or (y is not None and y < 0):
|
||||
self._center_is_zero = True
|
||||
params = self._position(x, y, z, e)
|
||||
@ -182,40 +204,46 @@ class GCodeReader(MeshReader):
|
||||
return position
|
||||
|
||||
def _processTCode(self, T, line, position, path):
|
||||
self._extruder = T
|
||||
if self._extruder + 1 > len(position.e):
|
||||
position.e.extend([0] * (self._extruder - len(position.e) + 1))
|
||||
if not self._is_layers_in_file:
|
||||
if len(path) > 1 and position[2] > 0:
|
||||
if self._createPolygon(position[2], path):
|
||||
self._layer += 1
|
||||
path.clear()
|
||||
else:
|
||||
path.clear()
|
||||
self._extruder_number = T
|
||||
if self._extruder_number + 1 > len(position.e):
|
||||
position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
|
||||
return position
|
||||
|
||||
_type_keyword = ";TYPE:"
|
||||
_layer_keyword = ";LAYER:"
|
||||
|
||||
## For showing correct x, y offsets for each extruder
|
||||
def _extruderOffsets(self):
|
||||
result = {}
|
||||
for extruder in ExtruderManager.getInstance().getExtruderStacks():
|
||||
result[int(extruder.getMetaData().get("position", "0"))] = [
|
||||
extruder.getProperty("machine_nozzle_offset_x", "value"),
|
||||
extruder.getProperty("machine_nozzle_offset_y", "value")]
|
||||
return result
|
||||
|
||||
def read(self, file_name):
|
||||
Logger.log("d", "Preparing to load %s" % file_name)
|
||||
self._cancelled = False
|
||||
|
||||
scene_node = SceneNode()
|
||||
scene_node.getBoundingBox = self._getNullBoundingBox # Manually set bounding box, because mesh doesn't have mesh data
|
||||
# Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
|
||||
# real data to calculate it from.
|
||||
scene_node.getBoundingBox = self._getNullBoundingBox
|
||||
|
||||
glist = []
|
||||
gcode_list = []
|
||||
self._is_layers_in_file = False
|
||||
|
||||
|
||||
Logger.log("d", "Opening file %s" % file_name)
|
||||
|
||||
self._extruder_offsets = self._extruderOffsets() # dict with index the extruder number. can be empty
|
||||
|
||||
last_z = 0
|
||||
with open(file_name, "r") as file:
|
||||
file_lines = 0
|
||||
current_line = 0
|
||||
for line in file:
|
||||
file_lines += 1
|
||||
glist.append(line)
|
||||
gcode_list.append(line)
|
||||
if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
|
||||
self._is_layers_in_file = True
|
||||
file.seek(0)
|
||||
@ -228,7 +256,7 @@ class GCodeReader(MeshReader):
|
||||
self._message.setProgress(0)
|
||||
self._message.show()
|
||||
|
||||
Logger.log("d", "Parsing %s" % file_name)
|
||||
Logger.log("d", "Parsing %s..." % file_name)
|
||||
|
||||
current_position = self._position(0, 0, 0, [0])
|
||||
current_path = []
|
||||
@ -238,10 +266,14 @@ class GCodeReader(MeshReader):
|
||||
Logger.log("d", "Parsing %s cancelled" % file_name)
|
||||
return None
|
||||
current_line += 1
|
||||
last_z = current_position.z
|
||||
|
||||
if current_line % file_step == 0:
|
||||
self._message.setProgress(math.floor(current_line / file_lines * 100))
|
||||
Job.yieldThread()
|
||||
if len(line) == 0:
|
||||
continue
|
||||
|
||||
if line.find(self._type_keyword) == 0:
|
||||
type = line[len(self._type_keyword):].strip()
|
||||
if type == "WALL-INNER":
|
||||
@ -256,28 +288,48 @@ class GCodeReader(MeshReader):
|
||||
self._layer_type = LayerPolygon.SupportType
|
||||
elif type == "FILL":
|
||||
self._layer_type = LayerPolygon.InfillType
|
||||
else:
|
||||
Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
|
||||
|
||||
if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
|
||||
try:
|
||||
layer_number = int(line[len(self._layer_keyword):])
|
||||
self._createPolygon(current_position[2], current_path)
|
||||
self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
|
||||
current_path.clear()
|
||||
self._layer = layer_number
|
||||
self._layer_number = layer_number
|
||||
except:
|
||||
pass
|
||||
if line[0] == ";":
|
||||
|
||||
# This line is a comment. Ignore it (except for the layer_keyword)
|
||||
if line.startswith(";"):
|
||||
continue
|
||||
|
||||
G = self._getInt(line, "G")
|
||||
if G is not None:
|
||||
current_position = self._processGCode(G, line, current_position, current_path)
|
||||
T = self._getInt(line, "T")
|
||||
if T is not None:
|
||||
current_position = self._processTCode(T, line, current_position, current_path)
|
||||
# < 2 is a heuristic for a movement only, that should not be counted as a layer
|
||||
if current_position.z > last_z and abs(current_position.z - last_z) < 2:
|
||||
if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
|
||||
current_path.clear()
|
||||
|
||||
if not self._is_layers_in_file and len(current_path) > 1 and current_position[2] > 0:
|
||||
if self._createPolygon(current_position[2], current_path):
|
||||
self._layer += 1
|
||||
current_path.clear()
|
||||
if not self._is_layers_in_file:
|
||||
self._layer_number += 1
|
||||
|
||||
continue
|
||||
|
||||
if line.startswith("T"):
|
||||
T = self._getInt(line, "T")
|
||||
if T is not None:
|
||||
self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
|
||||
current_path.clear()
|
||||
|
||||
current_position = self._processTCode(T, line, current_position, current_path)
|
||||
|
||||
# "Flush" leftovers
|
||||
if not self._is_layers_in_file and len(current_path) > 1:
|
||||
if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
|
||||
self._layer_number += 1
|
||||
current_path.clear()
|
||||
|
||||
material_color_map = numpy.zeros((10, 4), dtype = numpy.float32)
|
||||
material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
|
||||
@ -288,13 +340,13 @@ class GCodeReader(MeshReader):
|
||||
scene_node.addDecorator(decorator)
|
||||
|
||||
gcode_list_decorator = GCodeListDecorator()
|
||||
gcode_list_decorator.setGCodeList(glist)
|
||||
gcode_list_decorator.setGCodeList(gcode_list)
|
||||
scene_node.addDecorator(gcode_list_decorator)
|
||||
|
||||
Logger.log("d", "Finished parsing %s" % file_name)
|
||||
self._message.hide()
|
||||
|
||||
if self._layer == 0:
|
||||
if self._layer_number == 0:
|
||||
Logger.log("w", "File %s doesn't contain any valid layers" % file_name)
|
||||
|
||||
settings = Application.getInstance().getGlobalContainerStack()
|
||||
@ -306,4 +358,10 @@ class GCodeReader(MeshReader):
|
||||
|
||||
Logger.log("d", "Loaded %s" % file_name)
|
||||
|
||||
if Preferences.getInstance().getValue("gcodereader/show_caution"):
|
||||
caution_message = Message(catalog.i18nc(
|
||||
"@info:generic",
|
||||
"Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."), lifetime=0)
|
||||
caution_message.show()
|
||||
|
||||
return scene_node
|
||||
|
@ -21,7 +21,7 @@ class ImageReader(MeshReader):
|
||||
self._supported_extensions = [".jpg", ".jpeg", ".bmp", ".gif", ".png"]
|
||||
self._ui = ImageReaderUI(self)
|
||||
|
||||
def preRead(self, file_name):
|
||||
def preRead(self, file_name, *args, **kwargs):
|
||||
img = QImage(file_name)
|
||||
|
||||
if img.isNull():
|
||||
|
@ -37,7 +37,8 @@ class RemovableDriveOutputDevice(OutputDevice):
|
||||
# meshes.
|
||||
# \param limit_mimetypes Should we limit the available MIME types to the
|
||||
# MIME types available to the currently active machine?
|
||||
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None):
|
||||
#
|
||||
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
|
||||
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()
|
||||
@ -90,7 +91,7 @@ class RemovableDriveOutputDevice(OutputDevice):
|
||||
|
||||
self.writeStarted.emit(self)
|
||||
|
||||
job._message = message
|
||||
job.setMessage(message)
|
||||
self._writing = True
|
||||
job.start()
|
||||
except PermissionError as e:
|
||||
@ -117,8 +118,6 @@ class RemovableDriveOutputDevice(OutputDevice):
|
||||
raise OutputDeviceError.WriteRequestFailedError("Could not find a file name when trying to write to {device}.".format(device = self.getName()))
|
||||
|
||||
def _onProgress(self, job, progress):
|
||||
if hasattr(job, "_message"):
|
||||
job._message.setProgress(progress)
|
||||
self.writeProgress.emit(self, progress)
|
||||
|
||||
def _onFinished(self, job):
|
||||
@ -127,10 +126,6 @@ class RemovableDriveOutputDevice(OutputDevice):
|
||||
self._stream.close()
|
||||
self._stream = None
|
||||
|
||||
if hasattr(job, "_message"):
|
||||
job._message.hide()
|
||||
job._message = None
|
||||
|
||||
self._writing = False
|
||||
self.writeFinished.emit(self)
|
||||
if job.getResult():
|
||||
|
@ -35,6 +35,7 @@ class DiscoverUM3Action(MachineAction):
|
||||
@pyqtSlot()
|
||||
def startDiscovery(self):
|
||||
if not self._network_plugin:
|
||||
Logger.log("d", "Starting printer discovery.")
|
||||
self._network_plugin = Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting")
|
||||
self._network_plugin.printerListChanged.connect(self._onPrinterDiscoveryChanged)
|
||||
self.printersChanged.emit()
|
||||
@ -42,6 +43,7 @@ class DiscoverUM3Action(MachineAction):
|
||||
## Re-filters the list of printers.
|
||||
@pyqtSlot()
|
||||
def reset(self):
|
||||
Logger.log("d", "Reset the list of found printers.")
|
||||
self.printersChanged.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
@ -95,12 +97,14 @@ class DiscoverUM3Action(MachineAction):
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setKey(self, key):
|
||||
Logger.log("d", "Attempting to set the network key of the active machine to %s", key)
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
meta_data = global_container_stack.getMetaData()
|
||||
if "um_network_key" in meta_data:
|
||||
global_container_stack.setMetaDataEntry("um_network_key", key)
|
||||
# Delete old authentication data.
|
||||
Logger.log("d", "Removing old authentication id %s for device %s", global_container_stack.getMetaDataEntry("network_authentication_id", None), key)
|
||||
global_container_stack.removeMetaDataEntry("network_authentication_id")
|
||||
global_container_stack.removeMetaDataEntry("network_authentication_key")
|
||||
else:
|
||||
|
@ -200,11 +200,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
|
||||
|
||||
def _onAuthenticationRequired(self, reply, authenticator):
|
||||
if self._authentication_id is not None and self._authentication_key is not None:
|
||||
Logger.log("d", "Authentication was required. Setting up authenticator with ID %s",self._authentication_id )
|
||||
Logger.log("d", "Authentication was required. Setting up authenticator with ID %s and key", self._authentication_id, self._getSafeAuthKey())
|
||||
authenticator.setUser(self._authentication_id)
|
||||
authenticator.setPassword(self._authentication_key)
|
||||
else:
|
||||
Logger.log("d", "No authentication was required. The ID is: %s", self._authentication_id)
|
||||
Logger.log("d", "No authentication is available to use, but we did got a request for it.")
|
||||
|
||||
def getProperties(self):
|
||||
return self._properties
|
||||
@ -601,7 +601,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
|
||||
# This is ignored.
|
||||
# \param filter_by_machine Whether to filter MIME types by machine. This
|
||||
# is ignored.
|
||||
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None):
|
||||
# \param kwargs Keyword arguments.
|
||||
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
|
||||
if self._printer_state != "idle":
|
||||
self._error_message = Message(
|
||||
i18n_catalog.i18nc("@info:status", "Unable to start a new print job, printer is busy. Current printer status is %s.") % self._printer_state)
|
||||
@ -618,64 +619,67 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
|
||||
self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list")
|
||||
|
||||
print_information = Application.getInstance().getPrintInformation()
|
||||
|
||||
# Check if print cores / materials are loaded at all. Any failure in these results in an Error.
|
||||
for index in range(0, self._num_extruders):
|
||||
if print_information.materialLengths[index] != 0:
|
||||
if self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"] == "":
|
||||
Logger.log("e", "No cartridge loaded in slot %s, unable to start print", index + 1)
|
||||
self._error_message = Message(
|
||||
i18n_catalog.i18nc("@info:status", "Unable to start a new print job. No PrinterCore loaded in slot {0}".format(index + 1)))
|
||||
self._error_message.show()
|
||||
return
|
||||
if self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["guid"] == "":
|
||||
Logger.log("e", "No material loaded in slot %s, unable to start print", index + 1)
|
||||
self._error_message = Message(
|
||||
i18n_catalog.i18nc("@info:status",
|
||||
"Unable to start a new print job. No material loaded in slot {0}".format(index + 1)))
|
||||
self._error_message.show()
|
||||
return
|
||||
|
||||
warnings = [] # There might be multiple things wrong. Keep a list of all the stuff we need to warn about.
|
||||
|
||||
for index in range(0, self._num_extruders):
|
||||
# Check if there is enough material. Any failure in these results in a warning.
|
||||
material_length = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["length_remaining"]
|
||||
if material_length != -1 and print_information.materialLengths[index] > material_length:
|
||||
Logger.log("w", "Printer reports that there is not enough material left for extruder %s. We need %s and the printer has %s", index + 1, print_information.materialLengths[index], material_length)
|
||||
warnings.append(i18n_catalog.i18nc("@label", "Not enough material for spool {0}.").format(index+1))
|
||||
# Only check for mistakes if there is material length information.
|
||||
if print_information.materialLengths:
|
||||
# Check if print cores / materials are loaded at all. Any failure in these results in an Error.
|
||||
for index in range(0, self._num_extruders):
|
||||
if print_information.materialLengths[index] != 0:
|
||||
if self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"] == "":
|
||||
Logger.log("e", "No cartridge loaded in slot %s, unable to start print", index + 1)
|
||||
self._error_message = Message(
|
||||
i18n_catalog.i18nc("@info:status", "Unable to start a new print job. No PrinterCore loaded in slot {0}".format(index + 1)))
|
||||
self._error_message.show()
|
||||
return
|
||||
if self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["guid"] == "":
|
||||
Logger.log("e", "No material loaded in slot %s, unable to start print", index + 1)
|
||||
self._error_message = Message(
|
||||
i18n_catalog.i18nc("@info:status",
|
||||
"Unable to start a new print job. No material loaded in slot {0}".format(index + 1)))
|
||||
self._error_message.show()
|
||||
return
|
||||
|
||||
# Check if the right cartridges are loaded. Any failure in these results in a warning.
|
||||
extruder_manager = cura.Settings.ExtruderManager.ExtruderManager.getInstance()
|
||||
if print_information.materialLengths[index] != 0:
|
||||
variant = extruder_manager.getExtruderStack(index).findContainer({"type": "variant"})
|
||||
core_name = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"]
|
||||
if variant:
|
||||
if variant.getName() != core_name:
|
||||
Logger.log("w", "Extruder %s has a different Cartridge (%s) as Cura (%s)", index + 1, core_name, variant.getName())
|
||||
warnings.append(i18n_catalog.i18nc("@label", "Different print core (Cura: {0}, Printer: {1}) selected for extruder {2}".format(variant.getName(), core_name, index + 1)))
|
||||
for index in range(0, self._num_extruders):
|
||||
# Check if there is enough material. Any failure in these results in a warning.
|
||||
material_length = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["length_remaining"]
|
||||
if material_length != -1 and print_information.materialLengths[index] > material_length:
|
||||
Logger.log("w", "Printer reports that there is not enough material left for extruder %s. We need %s and the printer has %s", index + 1, print_information.materialLengths[index], material_length)
|
||||
warnings.append(i18n_catalog.i18nc("@label", "Not enough material for spool {0}.").format(index+1))
|
||||
|
||||
material = extruder_manager.getExtruderStack(index).findContainer({"type": "material"})
|
||||
if material:
|
||||
remote_material_guid = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["guid"]
|
||||
if material.getMetaDataEntry("GUID") != remote_material_guid:
|
||||
Logger.log("w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1,
|
||||
remote_material_guid,
|
||||
material.getMetaDataEntry("GUID"))
|
||||
# Check if the right cartridges are loaded. Any failure in these results in a warning.
|
||||
extruder_manager = cura.Settings.ExtruderManager.ExtruderManager.getInstance()
|
||||
if print_information.materialLengths[index] != 0:
|
||||
variant = extruder_manager.getExtruderStack(index).findContainer({"type": "variant"})
|
||||
core_name = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"]
|
||||
if variant:
|
||||
if variant.getName() != core_name:
|
||||
Logger.log("w", "Extruder %s has a different Cartridge (%s) as Cura (%s)", index + 1, core_name, variant.getName())
|
||||
warnings.append(i18n_catalog.i18nc("@label", "Different print core (Cura: {0}, Printer: {1}) selected for extruder {2}".format(variant.getName(), core_name, index + 1)))
|
||||
|
||||
remote_materials = UM.Settings.ContainerRegistry.ContainerRegistry.getInstance().findInstanceContainers(type = "material", GUID = remote_material_guid, read_only = True)
|
||||
remote_material_name = "Unknown"
|
||||
if remote_materials:
|
||||
remote_material_name = remote_materials[0].getName()
|
||||
warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(material.getName(), remote_material_name, index + 1))
|
||||
material = extruder_manager.getExtruderStack(index).findContainer({"type": "material"})
|
||||
if material:
|
||||
remote_material_guid = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["guid"]
|
||||
if material.getMetaDataEntry("GUID") != remote_material_guid:
|
||||
Logger.log("w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1,
|
||||
remote_material_guid,
|
||||
material.getMetaDataEntry("GUID"))
|
||||
|
||||
try:
|
||||
is_offset_calibrated = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["offset"]["state"] == "valid"
|
||||
except KeyError: # Older versions of the API don't expose the offset property, so we must asume that all is well.
|
||||
is_offset_calibrated = True
|
||||
remote_materials = UM.Settings.ContainerRegistry.ContainerRegistry.getInstance().findInstanceContainers(type = "material", GUID = remote_material_guid, read_only = True)
|
||||
remote_material_name = "Unknown"
|
||||
if remote_materials:
|
||||
remote_material_name = remote_materials[0].getName()
|
||||
warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(material.getName(), remote_material_name, index + 1))
|
||||
|
||||
if not is_offset_calibrated:
|
||||
warnings.append(i18n_catalog.i18nc("@label", "Print core {0} is not properly calibrated. XY calibration needs to be performed on the printer.").format(index + 1))
|
||||
try:
|
||||
is_offset_calibrated = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["offset"]["state"] == "valid"
|
||||
except KeyError: # Older versions of the API don't expose the offset property, so we must asume that all is well.
|
||||
is_offset_calibrated = True
|
||||
|
||||
if not is_offset_calibrated:
|
||||
warnings.append(i18n_catalog.i18nc("@label", "Print core {0} is not properly calibrated. XY calibration needs to be performed on the printer.").format(index + 1))
|
||||
else:
|
||||
Logger.log("w", "There was no material usage found. No check to match used material with machine is done.")
|
||||
|
||||
if warnings:
|
||||
text = i18n_catalog.i18nc("@label", "Are you sure you wish to print with the selected configuration?")
|
||||
@ -725,7 +729,12 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
|
||||
## Check if this machine was authenticated before.
|
||||
self._authentication_id = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("network_authentication_id", None)
|
||||
self._authentication_key = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("network_authentication_key", None)
|
||||
Logger.log("d", "Loaded authentication id %s from the metadata entry", self._authentication_id)
|
||||
|
||||
if self._authentication_id is None and self._authentication_key is None:
|
||||
Logger.log("d", "No authentication found in metadata.")
|
||||
else:
|
||||
Logger.log("d", "Loaded authentication id %s and key %s from the metadata entry", self._authentication_id, self._getSafeAuthKey())
|
||||
|
||||
self._update_timer.start()
|
||||
|
||||
## Stop requesting data from printer
|
||||
@ -841,7 +850,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
|
||||
|
||||
## Check if the authentication request was allowed by the printer.
|
||||
def _checkAuthentication(self):
|
||||
Logger.log("d", "Checking if authentication is correct for id %s", self._authentication_id)
|
||||
Logger.log("d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey())
|
||||
self._manager.get(QNetworkRequest(QUrl("http://" + self._address + self._api_prefix + "auth/check/" + str(self._authentication_id))))
|
||||
|
||||
## Request a authentication key from the printer so we can be authenticated
|
||||
@ -1010,7 +1019,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
|
||||
else:
|
||||
global_container_stack.addMetaDataEntry("network_authentication_id", self._authentication_id)
|
||||
Application.getInstance().saveStack(global_container_stack) # Force save so we are sure the data is not lost.
|
||||
Logger.log("i", "Authentication succeeded for id %s", self._authentication_id)
|
||||
Logger.log("i", "Authentication succeeded for id %s and key %s", self._authentication_id, self._getSafeAuthKey())
|
||||
else: # Got a response that we didn't expect, so something went wrong.
|
||||
Logger.log("e", "While trying to authenticate, we got an unexpected response: %s", reply.attribute(QNetworkRequest.HttpStatusCodeAttribute))
|
||||
self.setAuthenticationState(AuthState.NotAuthenticated)
|
||||
@ -1040,7 +1049,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
|
||||
|
||||
self._authentication_key = data["key"]
|
||||
self._authentication_id = data["id"]
|
||||
Logger.log("i", "Got a new authentication ID. Waiting for authorization: %s", self._authentication_id )
|
||||
Logger.log("i", "Got a new authentication ID (%s) and KEY (%S). Waiting for authorization.", self._authentication_id, self._getSafeAuthKey())
|
||||
|
||||
# Check if the authentication is accepted.
|
||||
self._checkAuthentication()
|
||||
@ -1110,3 +1119,12 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
|
||||
icon=QMessageBox.Question,
|
||||
callback=callback
|
||||
)
|
||||
|
||||
## Convenience function to "blur" out all but the last 5 characters of the auth key.
|
||||
# This can be used to debug print the key, without it compromising the security.
|
||||
def _getSafeAuthKey(self):
|
||||
if self._authentication_key is not None:
|
||||
result = self._authentication_key[-5:]
|
||||
result = "********" + result
|
||||
return result
|
||||
return self._authentication_key
|
@ -443,7 +443,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
||||
# This is ignored.
|
||||
# \param filter_by_machine Whether to filter MIME types by machine. This
|
||||
# is ignored.
|
||||
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None):
|
||||
# \param kwargs Keyword arguments.
|
||||
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
|
||||
container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
|
||||
if container_stack.getProperty("machine_gcode_flavor", "value") == "UltiGCode":
|
||||
|
@ -0,0 +1,77 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import configparser #To parse the files we need to upgrade and write the new files.
|
||||
import io #To serialise configparser output to a string.
|
||||
|
||||
from UM.VersionUpgrade import VersionUpgrade
|
||||
|
||||
_removed_settings = { #Settings that were removed in 2.5.
|
||||
"start_layers_at_same_position"
|
||||
}
|
||||
|
||||
## A collection of functions that convert the configuration of the user in Cura
|
||||
# 2.4 to a configuration for Cura 2.5.
|
||||
#
|
||||
# All of these methods are essentially stateless.
|
||||
class VersionUpgrade24to25(VersionUpgrade):
|
||||
## Gets the version number from a CFG file in Uranium's 2.4 format.
|
||||
#
|
||||
# Since the format may change, this is implemented for the 2.4 format only
|
||||
# and needs to be included in the version upgrade system rather than
|
||||
# globally in Uranium.
|
||||
#
|
||||
# \param serialised The serialised form of a CFG file.
|
||||
# \return The version number stored in the CFG file.
|
||||
# \raises ValueError The format of the version number in the file is
|
||||
# incorrect.
|
||||
# \raises KeyError The format of the file is incorrect.
|
||||
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.
|
||||
|
||||
## Upgrades the preferences file from version 2.4 to 2.5.
|
||||
#
|
||||
# \param serialised The serialised form of a preferences file.
|
||||
# \param filename The name of the file to upgrade.
|
||||
def upgradePreferences(self, serialised, filename):
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
parser.read_string(serialised)
|
||||
|
||||
#Remove settings from the visible_settings.
|
||||
if parser.has_section("general") and "visible_settings" in parser["general"]:
|
||||
visible_settings = parser["general"]["visible_settings"].split(";")
|
||||
visible_settings = filter(lambda setting: setting not in _removed_settings, visible_settings)
|
||||
parser["general"]["visible_settings"] = ";".join(visible_settings)
|
||||
|
||||
#Change the version number in the file.
|
||||
if parser.has_section("general"): #It better have!
|
||||
parser["general"]["version"] = "5"
|
||||
|
||||
#Re-serialise the file.
|
||||
output = io.StringIO()
|
||||
parser.write(output)
|
||||
return [filename], [output.getvalue()]
|
||||
|
||||
## Upgrades an instance container from version 2.4 to 2.5.
|
||||
#
|
||||
# \param serialised The serialised form of a quality profile.
|
||||
# \param filename The name of the file to upgrade.
|
||||
def upgradeInstanceContainer(self, serialised, filename):
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
parser.read_string(serialised)
|
||||
|
||||
#Remove settings from the [values] section.
|
||||
if parser.has_section("values"):
|
||||
for removed_setting in (_removed_settings & parser["values"].keys()): #Both in keys that need to be removed and in keys present in the file.
|
||||
del parser["values"][removed_setting]
|
||||
|
||||
#Change the version number in the file.
|
||||
if parser.has_section("general"):
|
||||
parser["general"]["version"] = "3"
|
||||
|
||||
#Re-serialise the file.
|
||||
output = io.StringIO()
|
||||
parser.write(output)
|
||||
return [filename], [output.getvalue()]
|
45
plugins/VersionUpgrade/VersionUpgrade24to25/__init__.py
Normal file
45
plugins/VersionUpgrade/VersionUpgrade24to25/__init__.py
Normal file
@ -0,0 +1,45 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from . import VersionUpgrade24to25
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
upgrade = VersionUpgrade24to25.VersionUpgrade24to25()
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "Version Upgrade 2.4 to 2.5"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Upgrades configurations from Cura 2.4 to Cura 2.5."),
|
||||
"api": 3
|
||||
},
|
||||
"version_upgrade": {
|
||||
# From To Upgrade function
|
||||
("preferences", 4): ("preferences", 5, upgrade.upgradePreferences),
|
||||
("quality", 2): ("quality", 3, upgrade.upgradeInstanceContainer),
|
||||
("variant", 2): ("variant", 3, upgrade.upgradeInstanceContainer), #We can re-use upgradeContainerStack since there is nothing specific to quality, variant or user profiles being changed.
|
||||
("user", 2): ("user", 3, upgrade.upgradeInstanceContainer)
|
||||
},
|
||||
"sources": {
|
||||
"quality": {
|
||||
"get_version": upgrade.getCfgVersion,
|
||||
"location": {"./quality"}
|
||||
},
|
||||
"preferences": {
|
||||
"get_version": upgrade.getCfgVersion,
|
||||
"location": {"."}
|
||||
},
|
||||
"user": {
|
||||
"get_version": upgrade.getCfgVersion,
|
||||
"location": {"./user"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def register(app):
|
||||
return {}
|
||||
return { "version_upgrade": upgrade }
|
@ -0,0 +1,190 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import configparser #To check whether the appropriate exceptions are raised.
|
||||
import pytest #To register tests with.
|
||||
|
||||
import VersionUpgrade24to25 #The module we're testing.
|
||||
|
||||
## Creates an instance of the upgrader to test with.
|
||||
@pytest.fixture
|
||||
def upgrader():
|
||||
return VersionUpgrade24to25.VersionUpgrade24to25()
|
||||
|
||||
test_cfg_version_good_data = [
|
||||
{
|
||||
"test_name": "Simple",
|
||||
"file_data": """[general]
|
||||
version = 1
|
||||
""",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"test_name": "Other Data Around",
|
||||
"file_data": """[nonsense]
|
||||
life = good
|
||||
|
||||
[general]
|
||||
version = 3
|
||||
|
||||
[values]
|
||||
layer_height = 0.12
|
||||
infill_sparse_density = 42
|
||||
""",
|
||||
"version": 3
|
||||
},
|
||||
{
|
||||
"test_name": "Negative Version", #Why not?
|
||||
"file_data": """[general]
|
||||
version = -20
|
||||
""",
|
||||
"version": -20
|
||||
}
|
||||
]
|
||||
|
||||
## Tests the technique that gets the version number from CFG files.
|
||||
#
|
||||
# \param data The parametrised data to test with. It contains a test name
|
||||
# to debug with, the serialised contents of a CFG file and the correct
|
||||
# version number in that CFG file.
|
||||
# \param upgrader The instance of the upgrade class to test.
|
||||
@pytest.mark.parametrize("data", test_cfg_version_good_data)
|
||||
def test_cfgVersionGood(data, upgrader):
|
||||
version = upgrader.getCfgVersion(data["file_data"])
|
||||
assert version == data["version"]
|
||||
|
||||
test_cfg_version_bad_data = [
|
||||
{
|
||||
"test_name": "Empty",
|
||||
"file_data": "",
|
||||
"exception": configparser.Error #Explicitly not specified further which specific error we're getting, because that depends on the implementation of configparser.
|
||||
},
|
||||
{
|
||||
"test_name": "No General",
|
||||
"file_data": """[values]
|
||||
layer_height = 0.1337
|
||||
""",
|
||||
"exception": configparser.Error
|
||||
},
|
||||
{
|
||||
"test_name": "No Version",
|
||||
"file_data": """[general]
|
||||
true = false
|
||||
""",
|
||||
"exception": configparser.Error
|
||||
},
|
||||
{
|
||||
"test_name": "Not a Number",
|
||||
"file_data": """[general]
|
||||
version = not-a-text-version-number
|
||||
""",
|
||||
"exception": ValueError
|
||||
}
|
||||
]
|
||||
|
||||
## Tests whether getting a version number from bad CFG files gives an
|
||||
# exception.
|
||||
#
|
||||
# \param data The parametrised data to test with. It contains a test name
|
||||
# to debug with, the serialised contents of a CFG file and the class of
|
||||
# exception it needs to throw.
|
||||
# \param upgrader The instance of the upgrader to test.
|
||||
@pytest.mark.parametrize("data", test_cfg_version_bad_data)
|
||||
def test_cfgVersionBad(data, upgrader):
|
||||
with pytest.raises(data["exception"]):
|
||||
upgrader.getCfgVersion(data["file_data"])
|
||||
|
||||
test_upgrade_preferences_removed_settings_data = [
|
||||
{
|
||||
"test_name": "Removed Setting",
|
||||
"file_data": """[general]
|
||||
visible_settings = baby;you;know;how;I;like;to;start_layers_at_same_position
|
||||
""",
|
||||
},
|
||||
{
|
||||
"test_name": "No Removed Setting",
|
||||
"file_data": """[general]
|
||||
visible_settings = baby;you;now;how;I;like;to;eat;chocolate;muffins
|
||||
"""
|
||||
},
|
||||
{
|
||||
"test_name": "No Visible Settings Key",
|
||||
"file_data": """[general]
|
||||
cura = cool
|
||||
"""
|
||||
},
|
||||
{
|
||||
"test_name": "No General Category",
|
||||
"file_data": """[foos]
|
||||
foo = bar
|
||||
"""
|
||||
}
|
||||
]
|
||||
|
||||
## Tests whether the settings that should be removed are removed for the 2.5
|
||||
# version of preferences.
|
||||
@pytest.mark.parametrize("data", test_upgrade_preferences_removed_settings_data)
|
||||
def test_upgradePreferencesRemovedSettings(data, upgrader):
|
||||
#Get the settings from the original file.
|
||||
original_parser = configparser.ConfigParser(interpolation = None)
|
||||
original_parser.read_string(data["file_data"])
|
||||
settings = set()
|
||||
if original_parser.has_section("general") and "visible_settings" in original_parser["general"]:
|
||||
settings = set(original_parser["general"]["visible_settings"].split(";"))
|
||||
|
||||
#Perform the upgrade.
|
||||
_, upgraded_preferences = upgrader.upgradePreferences(data["file_data"], "<string>")
|
||||
upgraded_preferences = upgraded_preferences[0]
|
||||
|
||||
#Find whether the removed setting is removed from the file now.
|
||||
settings -= VersionUpgrade24to25._removed_settings
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
parser.read_string(upgraded_preferences)
|
||||
assert (parser.has_section("general") and "visible_settings" in parser["general"]) == (len(settings) > 0) #If there are settings, there must also be a preference.
|
||||
if settings:
|
||||
assert settings == set(parser["general"]["visible_settings"].split(";"))
|
||||
|
||||
test_upgrade_instance_container_removed_settings_data = [
|
||||
{
|
||||
"test_name": "Removed Setting",
|
||||
"file_data": """[values]
|
||||
layer_height = 0.1337
|
||||
start_layers_at_same_position = True
|
||||
"""
|
||||
},
|
||||
{
|
||||
"test_name": "No Removed Setting",
|
||||
"file_data": """[values]
|
||||
oceans_number = 11
|
||||
"""
|
||||
},
|
||||
{
|
||||
"test_name": "No Values Category",
|
||||
"file_data": """[general]
|
||||
type = instance_container
|
||||
"""
|
||||
}
|
||||
]
|
||||
|
||||
## Tests whether the settings that should be removed are removed for the 2.5
|
||||
# version of instance containers.
|
||||
@pytest.mark.parametrize("data", test_upgrade_instance_container_removed_settings_data)
|
||||
def test_upgradeInstanceContainerRemovedSettings(data, upgrader):
|
||||
#Get the settings from the original file.
|
||||
original_parser = configparser.ConfigParser(interpolation = None)
|
||||
original_parser.read_string(data["file_data"])
|
||||
settings = set()
|
||||
if original_parser.has_section("values"):
|
||||
settings = set(original_parser["values"])
|
||||
|
||||
#Perform the upgrade.
|
||||
_, upgraded_container = upgrader.upgradeInstanceContainer(data["file_data"], "<string>")
|
||||
upgraded_container = upgraded_container[0]
|
||||
|
||||
#Find whether the forbidden setting is still in the container.
|
||||
settings -= VersionUpgrade24to25._removed_settings
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
parser.read_string(upgraded_container)
|
||||
assert parser.has_section("values") == (len(settings) > 0) #If there are settings, there must also be the values category.
|
||||
if settings:
|
||||
assert settings == set(parser["values"])
|
@ -248,11 +248,12 @@ class XmlMaterialProfile(InstanceContainer):
|
||||
|
||||
root = builder.close()
|
||||
_indent(root)
|
||||
stream = io.StringIO()
|
||||
stream = io.BytesIO()
|
||||
tree = ET.ElementTree(root)
|
||||
tree.write(stream, encoding="unicode", xml_declaration=True)
|
||||
# this makes sure that the XML header states encoding="utf-8"
|
||||
tree.write(stream, encoding="utf-8", xml_declaration=True)
|
||||
|
||||
return stream.getvalue()
|
||||
return stream.getvalue().decode('utf-8')
|
||||
|
||||
# Recursively resolve loading inherited files
|
||||
def _resolveInheritance(self, file_name):
|
||||
|
@ -1087,7 +1087,7 @@
|
||||
"default_value": 2,
|
||||
"minimum_value": "0",
|
||||
"minimum_value_warning": "infill_line_width",
|
||||
"value": "0 if infill_sparse_density == 0 else (infill_line_width * 100) / infill_sparse_density * (2 if infill_pattern == 'grid' else (3 if infill_pattern == 'triangles' or infill_pattern == 'cubic' or infill_pattern == 'cubicsubdiv' else (4 if infill_pattern == 'tetrahedral' else 1)))",
|
||||
"value": "0 if infill_sparse_density == 0 else (infill_line_width * 100) / infill_sparse_density * (2 if infill_pattern == 'grid' else (3 if infill_pattern == 'triangles' or infill_pattern == 'cubic' or infill_pattern == 'cubicsubdiv' else (2 if infill_pattern == 'tetrahedral' else 1)))",
|
||||
"settable_per_mesh": true
|
||||
}
|
||||
}
|
||||
@ -1321,7 +1321,7 @@
|
||||
"max_skin_angle_for_expansion":
|
||||
{
|
||||
"label": "Maximum Skin Angle for Expansion",
|
||||
"description": "Top and or bottom surfaces of your object with an angle larger than this setting, won't have their top/bottom skin expanded. This avoids expanding the narrow skin areas that are created when the model surface has a near vertical slope. An angle of 0° is horizontal, while an angle of 90° is vertical.",
|
||||
"description": "Top and/or bottom surfaces of your object with an angle larger than this setting, won't have their top/bottom skin expanded. This avoids expanding the narrow skin areas that are created when the model surface has a near vertical slope. An angle of 0° is horizontal, while an angle of 90° is vertical.",
|
||||
"unit": "°",
|
||||
"type": "float",
|
||||
"minimum_value": "0",
|
||||
@ -2556,6 +2556,17 @@
|
||||
"settable_per_mesh": false,
|
||||
"settable_per_extruder": true
|
||||
},
|
||||
"start_layers_at_same_position":
|
||||
{
|
||||
"label": "Start Layers with the Same Part",
|
||||
"description": "In each layer start with printing the object near the same point, so that we don't start a new layer with printing the piece which the previous layer ended with. This makes for better overhangs and small parts, but increases printing time.",
|
||||
"type": "bool",
|
||||
"default_value": false,
|
||||
"enabled": false,
|
||||
"settable_per_mesh": false,
|
||||
"settable_per_extruder": false,
|
||||
"settable_per_meshgroup": true
|
||||
},
|
||||
"layer_start_x":
|
||||
{
|
||||
"label": "Layer Start X",
|
||||
|
129
resources/definitions/makeit_pro_l.def.json
Normal file
129
resources/definitions/makeit_pro_l.def.json
Normal file
@ -0,0 +1,129 @@
|
||||
{
|
||||
"id": "makeit_pro_l",
|
||||
"version": 2,
|
||||
"name": "MAKEiT Pro-L",
|
||||
"inherits": "fdmprinter",
|
||||
"metadata": {
|
||||
"visible": true,
|
||||
"author": "NA",
|
||||
"manufacturer": "NA",
|
||||
"category": "Other",
|
||||
"file_formats": "text/x-gcode",
|
||||
"has_materials": false,
|
||||
"supported_actions": [ "MachineSettingsAction", "UpgradeFirmware" ],
|
||||
"machine_extruder_trains":
|
||||
{
|
||||
"0": "makeit_l_dual_1st",
|
||||
"1": "makeit_l_dual_2nd"
|
||||
}
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_name": { "default_value": "MAKEiT Pro-L" },
|
||||
"machine_width": {
|
||||
"default_value": 305
|
||||
},
|
||||
"machine_height": {
|
||||
"default_value": 330
|
||||
},
|
||||
"machine_depth": {
|
||||
"default_value": 254
|
||||
},
|
||||
"machine_center_is_zero": {
|
||||
"default_value": false
|
||||
},
|
||||
"machine_nozzle_size": {
|
||||
"default_value": 0.4
|
||||
},
|
||||
"machine_nozzle_heat_up_speed": {
|
||||
"default_value": 2
|
||||
},
|
||||
"machine_nozzle_cool_down_speed": {
|
||||
"default_value": 2
|
||||
},
|
||||
"machine_head_with_fans_polygon":
|
||||
{
|
||||
"default_value": [
|
||||
[ -305, 28 ],
|
||||
[ -305, -28 ],
|
||||
[ 305, 28 ],
|
||||
[ 305, -28 ]
|
||||
]
|
||||
},
|
||||
"gantry_height": {
|
||||
"default_value": 330
|
||||
},
|
||||
"machine_use_extruder_offset_to_offset_coords": {
|
||||
"default_value": true
|
||||
},
|
||||
"machine_gcode_flavor": {
|
||||
"default_value": "RepRap (Marlin/Sprinter)"
|
||||
},
|
||||
"machine_start_gcode": {
|
||||
"default_value": "G21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nG92 E0 ;zero the extruded length\nG28 ;home\nG1 F200 E30 ;extrude 30 mm of feed stock\nG92 E0 ;zero the extruded length\nG1 E-5 ;retract 5 mm\nG28 SC ;Do homeing, clean nozzles and let printer to know that printing started\nG92 X-6 ;Sets Curas checker board to match printers heated bed coordinates\nG1 F{speed_travel}\nM117 Printing..."
|
||||
},
|
||||
"machine_end_gcode": {
|
||||
"default_value": "M104 T0 S0 ;1st extruder heater off\nM104 T1 S0 ;2nd extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-5 F9000 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+5 X+20 Y+20 F9000 ;move Z up a bit\nM117 MAKEiT Pro@Done\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning\nM81"
|
||||
},
|
||||
"machine_extruder_count": {
|
||||
"default_value": 2
|
||||
},
|
||||
"print_sequence": {
|
||||
"enabled": true
|
||||
},
|
||||
"prime_tower_position_x": {
|
||||
"default_value": 185
|
||||
},
|
||||
"prime_tower_position_y": {
|
||||
"default_value": 160
|
||||
},
|
||||
"material_diameter": {
|
||||
"default_value": 1.75
|
||||
},
|
||||
"layer_height": {
|
||||
"default_value": 0.2
|
||||
},
|
||||
"retraction_speed": {
|
||||
"default_value": 180
|
||||
},
|
||||
"infill_sparse_density": {
|
||||
"default_value": 20
|
||||
},
|
||||
"retraction_amount": {
|
||||
"default_value": 6
|
||||
},
|
||||
"retraction_min_travel": {
|
||||
"default_value": 1.5
|
||||
},
|
||||
"speed_travel": {
|
||||
"default_value": 150
|
||||
},
|
||||
"speed_print": {
|
||||
"default_value": 60
|
||||
},
|
||||
"wall_thickness": {
|
||||
"default_value": 1.2
|
||||
},
|
||||
"bottom_thickness": {
|
||||
"default_value": 0.2
|
||||
},
|
||||
"speed_layer_0": {
|
||||
"default_value": 20
|
||||
},
|
||||
"speed_print_layer_0": {
|
||||
"default_value": 20
|
||||
},
|
||||
"cool_min_layer_time_fan_speed_max": {
|
||||
"default_value": 5
|
||||
},
|
||||
"adhesion_type": {
|
||||
"default_value": "skirt"
|
||||
},
|
||||
"machine_heated_bed": {
|
||||
"default_value": true
|
||||
},
|
||||
"machine_heat_zone_length": {
|
||||
"default_value": 20
|
||||
}
|
||||
}
|
||||
}
|
126
resources/definitions/makeit_pro_m.def.json
Normal file
126
resources/definitions/makeit_pro_m.def.json
Normal file
@ -0,0 +1,126 @@
|
||||
{
|
||||
"id": "makeit_pro_m",
|
||||
"version": 2,
|
||||
"name": "MAKEiT Pro-M",
|
||||
"inherits": "fdmprinter",
|
||||
"metadata": {
|
||||
"visible": true,
|
||||
"author": "NA",
|
||||
"manufacturer": "NA",
|
||||
"category": "Other",
|
||||
"file_formats": "text/x-gcode",
|
||||
"has_materials": false,
|
||||
"supported_actions": [ "MachineSettingsAction", "UpgradeFirmware" ],
|
||||
"machine_extruder_trains":
|
||||
{
|
||||
"0": "makeit_dual_1st",
|
||||
"1": "makeit_dual_2nd"
|
||||
}
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_name": { "default_value": "MAKEiT Pro-M" },
|
||||
"machine_width": {
|
||||
"default_value": 200
|
||||
},
|
||||
"machine_height": {
|
||||
"default_value": 200
|
||||
},
|
||||
"machine_depth": {
|
||||
"default_value": 240
|
||||
},
|
||||
"machine_center_is_zero": {
|
||||
"default_value": false
|
||||
},
|
||||
"machine_nozzle_size": {
|
||||
"default_value": 0.4
|
||||
},
|
||||
"machine_nozzle_heat_up_speed": {
|
||||
"default_value": 2
|
||||
},
|
||||
"machine_nozzle_cool_down_speed": {
|
||||
"default_value": 2
|
||||
},
|
||||
"machine_head_with_fans_polygon":
|
||||
{
|
||||
"default_value": [
|
||||
[ -200, 240 ],
|
||||
[ -200, -32 ],
|
||||
[ 200, 240 ],
|
||||
[ 200, -32 ]
|
||||
]
|
||||
},
|
||||
"gantry_height": {
|
||||
"default_value": 200
|
||||
},
|
||||
"machine_use_extruder_offset_to_offset_coords": {
|
||||
"default_value": true
|
||||
},
|
||||
"machine_gcode_flavor": {
|
||||
"default_value": "RepRap (Marlin/Sprinter)"
|
||||
},
|
||||
"machine_start_gcode": {
|
||||
"default_value": "G21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nG92 E0 ;zero the extruded length\nG28 ;home\nG1 F200 E30 ;extrude 30 mm of feed stock\nG92 E0 ;zero the extruded length\nG1 E-5 ;retract 5 mm\nG28 SC ;Do homeing, clean nozzles and let printer to know that printing started\nG92 X-6 ;Sets Curas checker board to match printers heated bed coordinates\nG1 F{speed_travel}\nM117 Printing..."
|
||||
},
|
||||
"machine_end_gcode": {
|
||||
"default_value": "M104 T0 S0 ;1st extruder heater off\nM104 T1 S0 ;2nd extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-5 F9000 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+5 X+20 Y+20 F9000 ;move Z up a bit\nM117 MAKEiT Pro@Done\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning\nM81"
|
||||
},
|
||||
"machine_extruder_count": {
|
||||
"default_value": 2
|
||||
},
|
||||
"print_sequence": {
|
||||
"enabled": false
|
||||
},
|
||||
"prime_tower_position_x": {
|
||||
"default_value": 185
|
||||
},
|
||||
"prime_tower_position_y": {
|
||||
"default_value": 160
|
||||
},
|
||||
"material_diameter": {
|
||||
"default_value": 1.75
|
||||
},
|
||||
"layer_height": {
|
||||
"default_value": 0.2
|
||||
},
|
||||
"retraction_speed": {
|
||||
"default_value": 180
|
||||
},
|
||||
"infill_sparse_density": {
|
||||
"default_value": 20
|
||||
},
|
||||
"retraction_amount": {
|
||||
"default_value": 6
|
||||
},
|
||||
"retraction_min_travel": {
|
||||
"default_value": 1.5
|
||||
},
|
||||
"speed_travel": {
|
||||
"default_value": 150
|
||||
},
|
||||
"speed_print": {
|
||||
"default_value": 60
|
||||
},
|
||||
"wall_thickness": {
|
||||
"default_value": 1.2
|
||||
},
|
||||
"bottom_thickness": {
|
||||
"default_value": 0.2
|
||||
},
|
||||
"speed_layer_0": {
|
||||
"default_value": 20
|
||||
},
|
||||
"speed_print_layer_0": {
|
||||
"default_value": 20
|
||||
},
|
||||
"cool_min_layer_time_fan_speed_max": {
|
||||
"default_value": 5
|
||||
},
|
||||
"adhesion_type": {
|
||||
"default_value": "skirt"
|
||||
},
|
||||
"machine_heated_bed": {
|
||||
"default_value": true
|
||||
}
|
||||
}
|
||||
}
|
@ -58,7 +58,7 @@
|
||||
"value": "100"
|
||||
},
|
||||
"machine_end_gcode": {
|
||||
"default_value": ";End GCode\nM104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-5 X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning"
|
||||
"default_value": ";End GCode\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 E-5 X-20 Y-20 ;retract filament even more\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\nG0 Z{machine_height} ;move the platform all the way down\nM104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nM84 ;steppers off\nG90 ;absolute positioning\nM117 Done"
|
||||
},
|
||||
"machine_gcode_flavor": {
|
||||
"default_value": "RepRap (Marlin/Sprinter)"
|
||||
@ -70,7 +70,7 @@
|
||||
"default_value": "Renkforce RF100"
|
||||
},
|
||||
"machine_start_gcode": {
|
||||
"default_value": ";Sliced at: {day} {date} {time}\nG21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 X0 Y0 ;move X/Y to min endstops\nG28 Z0 ;move Z to min endstops\nG1 Z15.0 F{speed_travel} ;move the platform down 15mm\nG92 E0 ;zero the extruded length\nG1 F200 E3 ;extrude 3mm of feed stock\nG92 E0 ;zero the extruded length again\nG1 F{speed_travel}\nM117 Printing..."
|
||||
"default_value": ";Start GCode\nG21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 X0 Y0 ;move X/Y to min endstops\nG28 Z0 ;move Z to min endstops\nG1 Z15.0 ;move the platform down 15mm\nG92 E0 ;zero the extruded length\nG1 F200 E3 ;extrude 3mm of feed stock\nG92 E0 ;zero the extruded length again\n;Put printing message on LCD screen\nM117 Printing..."
|
||||
},
|
||||
"machine_width": {
|
||||
"value": "100"
|
||||
|
26
resources/extruders/makeit_dual_1st.def.json
Normal file
26
resources/extruders/makeit_dual_1st.def.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "makeit_dual_1st",
|
||||
"version": 2,
|
||||
"name": "1st Extruder",
|
||||
"inherits": "fdmextruder",
|
||||
"metadata": {
|
||||
"machine": "makeit_pro_m",
|
||||
"position": "0"
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"extruder_nr": {
|
||||
"default_value": 0,
|
||||
"maximum_value": "1"
|
||||
},
|
||||
"machine_nozzle_offset_x": { "default_value": 0.0 },
|
||||
"machine_nozzle_offset_y": { "default_value": 0.0 },
|
||||
|
||||
"machine_extruder_start_pos_abs": { "default_value": true },
|
||||
"machine_extruder_start_pos_x": { "value": "prime_tower_position_x" },
|
||||
"machine_extruder_start_pos_y": { "value": "prime_tower_position_y" },
|
||||
"machine_extruder_end_pos_abs": { "default_value": true },
|
||||
"machine_extruder_end_pos_x": { "value": "prime_tower_position_x" },
|
||||
"machine_extruder_end_pos_y": { "value": "prime_tower_position_y" }
|
||||
}
|
||||
}
|
26
resources/extruders/makeit_dual_2nd.def.json
Normal file
26
resources/extruders/makeit_dual_2nd.def.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "makeit_dual_2nd",
|
||||
"version": 2,
|
||||
"name": "2nd Extruder",
|
||||
"inherits": "fdmextruder",
|
||||
"metadata": {
|
||||
"machine": "makeit_pro_m",
|
||||
"position": "1"
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"extruder_nr": {
|
||||
"default_value": 1,
|
||||
"maximum_value": "1"
|
||||
},
|
||||
"machine_nozzle_offset_x": { "default_value": 0.0 },
|
||||
"machine_nozzle_offset_y": { "default_value": 0.0 },
|
||||
|
||||
"machine_extruder_start_pos_abs": { "default_value": true },
|
||||
"machine_extruder_start_pos_x": { "value": "prime_tower_position_x" },
|
||||
"machine_extruder_start_pos_y": { "value": "prime_tower_position_y" },
|
||||
"machine_extruder_end_pos_abs": { "default_value": true },
|
||||
"machine_extruder_end_pos_x": { "value": "prime_tower_position_x" },
|
||||
"machine_extruder_end_pos_y": { "value": "prime_tower_position_y" }
|
||||
}
|
||||
}
|
26
resources/extruders/makeit_l_dual_1st.def.json
Normal file
26
resources/extruders/makeit_l_dual_1st.def.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "makeit_l_dual_1st",
|
||||
"version": 2,
|
||||
"name": "1st Extruder",
|
||||
"inherits": "fdmextruder",
|
||||
"metadata": {
|
||||
"machine": "makeit_pro_l",
|
||||
"position": "0"
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"extruder_nr": {
|
||||
"default_value": 0,
|
||||
"maximum_value": "1"
|
||||
},
|
||||
"machine_nozzle_offset_x": { "default_value": 0.0 },
|
||||
"machine_nozzle_offset_y": { "default_value": 0.0 },
|
||||
|
||||
"machine_extruder_start_pos_abs": { "default_value": true },
|
||||
"machine_extruder_start_pos_x": { "value": "prime_tower_position_x" },
|
||||
"machine_extruder_start_pos_y": { "value": "prime_tower_position_y" },
|
||||
"machine_extruder_end_pos_abs": { "default_value": true },
|
||||
"machine_extruder_end_pos_x": { "value": "prime_tower_position_x" },
|
||||
"machine_extruder_end_pos_y": { "value": "prime_tower_position_y" }
|
||||
}
|
||||
}
|
26
resources/extruders/makeit_l_dual_2nd.def.json
Normal file
26
resources/extruders/makeit_l_dual_2nd.def.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "makeit_l_dual_2nd",
|
||||
"version": 2,
|
||||
"name": "2nd Extruder",
|
||||
"inherits": "fdmextruder",
|
||||
"metadata": {
|
||||
"machine": "makeit_pro_l",
|
||||
"position": "1"
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"extruder_nr": {
|
||||
"default_value": 1,
|
||||
"maximum_value": "1"
|
||||
},
|
||||
"machine_nozzle_offset_x": { "default_value": 0.0 },
|
||||
"machine_nozzle_offset_y": { "default_value": 0.0 },
|
||||
|
||||
"machine_extruder_start_pos_abs": { "default_value": true },
|
||||
"machine_extruder_start_pos_x": { "value": "prime_tower_position_x" },
|
||||
"machine_extruder_start_pos_y": { "value": "prime_tower_position_y" },
|
||||
"machine_extruder_end_pos_abs": { "default_value": true },
|
||||
"machine_extruder_end_pos_x": { "value": "prime_tower_position_x" },
|
||||
"machine_extruder_end_pos_y": { "value": "prime_tower_position_y" }
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@
|
||||
"machine_extruder_end_pos_x": { "default_value": 213 },
|
||||
"machine_extruder_end_pos_y": { "default_value": 207 },
|
||||
"machine_nozzle_head_distance": { "default_value": 2.7 },
|
||||
"extruder_prime_pos_x": { "default_value": 170 },
|
||||
"extruder_prime_pos_x": { "default_value": 9 },
|
||||
"extruder_prime_pos_y": { "default_value": 6 },
|
||||
"extruder_prime_pos_z": { "default_value": 2 }
|
||||
}
|
||||
|
@ -23,7 +23,7 @@
|
||||
"machine_extruder_end_pos_x": { "default_value": 213 },
|
||||
"machine_extruder_end_pos_y": { "default_value": 189 },
|
||||
"machine_nozzle_head_distance": { "default_value": 4.2 },
|
||||
"extruder_prime_pos_x": { "default_value": 182 },
|
||||
"extruder_prime_pos_x": { "default_value": 222 },
|
||||
"extruder_prime_pos_y": { "default_value": 6 },
|
||||
"extruder_prime_pos_z": { "default_value": 2 }
|
||||
}
|
||||
|
@ -23,7 +23,7 @@
|
||||
"machine_extruder_end_pos_x": { "default_value": 213 },
|
||||
"machine_extruder_end_pos_y": { "default_value": 207 },
|
||||
"machine_nozzle_head_distance": { "default_value": 2.7 },
|
||||
"extruder_prime_pos_x": { "default_value": 170 },
|
||||
"extruder_prime_pos_x": { "default_value": 9 },
|
||||
"extruder_prime_pos_y": { "default_value": 6 },
|
||||
"extruder_prime_pos_z": { "default_value": 2 }
|
||||
}
|
||||
|
@ -23,7 +23,7 @@
|
||||
"machine_extruder_end_pos_x": { "default_value": 213 },
|
||||
"machine_extruder_end_pos_y": { "default_value": 189 },
|
||||
"machine_nozzle_head_distance": { "default_value": 4.2 },
|
||||
"extruder_prime_pos_x": { "default_value": 182 },
|
||||
"extruder_prime_pos_x": { "default_value": 222 },
|
||||
"extruder_prime_pos_y": { "default_value": 6 },
|
||||
"extruder_prime_pos_z": { "default_value": 2 }
|
||||
}
|
||||
|
@ -10,8 +10,8 @@ import Cura 1.0 as Cura
|
||||
|
||||
Item
|
||||
{
|
||||
property alias newProject: newProjectAction;
|
||||
property alias open: openAction;
|
||||
property alias loadWorkspace: loadWorkspaceAction;
|
||||
property alias quit: quitAction;
|
||||
|
||||
property alias undo: undoAction;
|
||||
@ -283,15 +283,16 @@ Item
|
||||
Action
|
||||
{
|
||||
id: openAction;
|
||||
text: catalog.i18nc("@action:inmenu menubar:file","&Open File...");
|
||||
text: catalog.i18nc("@action:inmenu menubar:file","&Open File(s)...");
|
||||
iconName: "document-open";
|
||||
shortcut: StandardKey.Open;
|
||||
}
|
||||
|
||||
Action
|
||||
{
|
||||
id: loadWorkspaceAction
|
||||
text: catalog.i18nc("@action:inmenu menubar:file","&Open Project...");
|
||||
id: newProjectAction
|
||||
text: catalog.i18nc("@action:inmenu menubar:file","&New Project...");
|
||||
shortcut: StandardKey.New
|
||||
}
|
||||
|
||||
Action
|
||||
|
126
resources/qml/AskOpenAsProjectOrModelsDialog.qml
Normal file
126
resources/qml/AskOpenAsProjectOrModelsDialog.qml
Normal file
@ -0,0 +1,126 @@
|
||||
// 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.1
|
||||
import QtQuick.Controls.Styles 1.1
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Dialogs 1.1
|
||||
|
||||
import UM 1.3 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
||||
|
||||
UM.Dialog
|
||||
{
|
||||
// This dialog asks the user whether he/she wants to open a project file as a project or import models.
|
||||
id: base
|
||||
|
||||
title: catalog.i18nc("@title:window", "Open project file")
|
||||
width: 420
|
||||
height: 140
|
||||
|
||||
maximumHeight: height
|
||||
maximumWidth: width
|
||||
minimumHeight: height
|
||||
minimumWidth: width
|
||||
|
||||
modality: UM.Application.platform == "linux" ? Qt.NonModal : Qt.WindowModal;
|
||||
|
||||
property var fileUrl
|
||||
|
||||
function loadProjectFile(projectFile)
|
||||
{
|
||||
UM.WorkspaceFileHandler.readLocalFile(projectFile);
|
||||
|
||||
var meshName = backgroundItem.getMeshName(projectFile.toString());
|
||||
backgroundItem.hasMesh(decodeURIComponent(meshName));
|
||||
}
|
||||
|
||||
function loadModelFiles(fileUrls)
|
||||
{
|
||||
for (var i in fileUrls)
|
||||
Printer.readLocalFile(fileUrls[i]);
|
||||
|
||||
var meshName = backgroundItem.getMeshName(fileUrls[0].toString());
|
||||
backgroundItem.hasMesh(decodeURIComponent(meshName));
|
||||
}
|
||||
|
||||
onVisibleChanged:
|
||||
{
|
||||
if (visible)
|
||||
{
|
||||
var rememberMyChoice = UM.Preferences.getValue("cura/choice_on_open_project") != "always_ask";
|
||||
rememberChoiceCheckBox.checked = rememberMyChoice;
|
||||
}
|
||||
}
|
||||
|
||||
Column
|
||||
{
|
||||
anchors.fill: parent
|
||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@text:window", "This is a Cura project file. Would you like to open it as a project\nor import the models from it?")
|
||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||
font: UM.Theme.getFont("default")
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
CheckBox
|
||||
{
|
||||
id: rememberChoiceCheckBox
|
||||
text: catalog.i18nc("@text:window", "Remember my choice")
|
||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||
checked: UM.Preferences.getValue("cura/choice_on_open_project") != "always_ask"
|
||||
}
|
||||
|
||||
// Buttons
|
||||
Item
|
||||
{
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
height: childrenRect.height
|
||||
|
||||
Button
|
||||
{
|
||||
id: openAsProjectButton
|
||||
text: catalog.i18nc("@action:button", "Open as project");
|
||||
anchors.right: importModelsButton.left
|
||||
anchors.rightMargin: UM.Theme.getSize("default_margin").width
|
||||
isDefault: true
|
||||
onClicked:
|
||||
{
|
||||
// update preference
|
||||
if (rememberChoiceCheckBox.checked)
|
||||
UM.Preferences.setValue("cura/choice_on_open_project", "open_as_project");
|
||||
|
||||
// load this file as project
|
||||
base.hide();
|
||||
loadProjectFile(base.fileUrl);
|
||||
}
|
||||
}
|
||||
|
||||
Button
|
||||
{
|
||||
id: importModelsButton
|
||||
text: catalog.i18nc("@action:button", "Import models");
|
||||
anchors.right: parent.right
|
||||
onClicked:
|
||||
{
|
||||
// update preference
|
||||
if (rememberChoiceCheckBox.checked)
|
||||
UM.Preferences.setValue("cura/choice_on_open_project", "open_as_model");
|
||||
|
||||
// load models from this project file
|
||||
base.hide();
|
||||
loadModelFiles([base.fileUrl]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -66,6 +66,10 @@ UM.MainWindow
|
||||
{
|
||||
id: fileMenu
|
||||
title: catalog.i18nc("@title:menu menubar:toplevel","&File");
|
||||
MenuItem
|
||||
{
|
||||
action: Cura.Actions.newProject;
|
||||
}
|
||||
|
||||
MenuItem
|
||||
{
|
||||
@ -74,11 +78,6 @@ UM.MainWindow
|
||||
|
||||
RecentFilesMenu { }
|
||||
|
||||
MenuItem
|
||||
{
|
||||
action: Cura.Actions.loadWorkspace
|
||||
}
|
||||
|
||||
MenuSeparator { }
|
||||
|
||||
MenuItem
|
||||
@ -86,28 +85,20 @@ UM.MainWindow
|
||||
text: catalog.i18nc("@action:inmenu menubar:file", "&Save Selection to File");
|
||||
enabled: UM.Selection.hasSelection;
|
||||
iconName: "document-save-as";
|
||||
onTriggered: UM.OutputDeviceManager.requestWriteSelectionToDevice("local_file", PrintInformation.jobName, { "filter_by_machine": false });
|
||||
onTriggered: UM.OutputDeviceManager.requestWriteSelectionToDevice("local_file", PrintInformation.jobName, { "filter_by_machine": false, "preferred_mimetype": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml"});
|
||||
}
|
||||
Menu
|
||||
|
||||
MenuItem
|
||||
{
|
||||
id: saveAllMenu
|
||||
title: catalog.i18nc("@title:menu menubar:file","Save &All")
|
||||
iconName: "document-save-all";
|
||||
enabled: devicesModel.rowCount() > 0 && UM.Backend.progress > 0.99;
|
||||
|
||||
Instantiator
|
||||
id: saveAsMenu
|
||||
text: catalog.i18nc("@title:menu menubar:file", "Save &As...")
|
||||
onTriggered:
|
||||
{
|
||||
model: UM.OutputDevicesModel { id: devicesModel; }
|
||||
|
||||
MenuItem
|
||||
{
|
||||
text: model.description;
|
||||
onTriggered: UM.OutputDeviceManager.requestWriteToDevice(model.id, PrintInformation.jobName, { "filter_by_machine": false });
|
||||
}
|
||||
onObjectAdded: saveAllMenu.insertItem(index, object)
|
||||
onObjectRemoved: saveAllMenu.removeItem(object)
|
||||
var localDeviceId = "local_file";
|
||||
UM.OutputDeviceManager.requestWriteToDevice(localDeviceId, PrintInformation.jobName, { "filter_by_machine": false, "preferred_mimetype": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml"});
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem
|
||||
{
|
||||
id: saveWorkspaceMenu
|
||||
@ -534,6 +525,33 @@ UM.MainWindow
|
||||
onTriggered: preferences.visible = true
|
||||
}
|
||||
|
||||
MessageDialog
|
||||
{
|
||||
id: newProjectDialog
|
||||
modality: Qt.ApplicationModal
|
||||
title: catalog.i18nc("@title:window", "New project")
|
||||
text: catalog.i18nc("@info:question", "Are you sure you want to start a new project? This will clear the build plate and any unsaved settings.")
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
icon: StandardIcon.Question
|
||||
onYes:
|
||||
{
|
||||
Printer.deleteAll();
|
||||
Cura.Actions.resetProfile.trigger();
|
||||
}
|
||||
}
|
||||
|
||||
Connections
|
||||
{
|
||||
target: Cura.Actions.newProject
|
||||
onTriggered:
|
||||
{
|
||||
if(Printer.platformActivity || Cura.MachineManager.hasUserSettings)
|
||||
{
|
||||
newProjectDialog.visible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections
|
||||
{
|
||||
target: Cura.Actions.addProfile
|
||||
@ -721,27 +739,100 @@ UM.MainWindow
|
||||
id: openDialog;
|
||||
|
||||
//: File open dialog title
|
||||
title: catalog.i18nc("@title:window","Open file")
|
||||
title: catalog.i18nc("@title:window","Open file(s)")
|
||||
modality: UM.Application.platform == "linux" ? Qt.NonModal : Qt.WindowModal;
|
||||
selectMultiple: true
|
||||
nameFilters: UM.MeshFileHandler.supportedReadFileTypes;
|
||||
folder: CuraApplication.getDefaultPath("dialog_load_path")
|
||||
onAccepted:
|
||||
{
|
||||
//Because several implementations of the file dialog only update the folder
|
||||
//when it is explicitly set.
|
||||
// Because several implementations of the file dialog only update the folder
|
||||
// when it is explicitly set.
|
||||
var f = folder;
|
||||
folder = f;
|
||||
|
||||
CuraApplication.setDefaultPath("dialog_load_path", folder);
|
||||
|
||||
for(var i in fileUrls)
|
||||
// look for valid project files
|
||||
var projectFileUrlList = [];
|
||||
var hasGcode = false;
|
||||
for (var i in fileUrls)
|
||||
{
|
||||
Printer.readLocalFile(fileUrls[i])
|
||||
var endsWithG = /\.g$/;
|
||||
var endsWithGcode = /\.gcode$/;
|
||||
if (endsWithG.test(fileUrls[i]) || endsWithGcode.test(fileUrls[i])) {
|
||||
hasGcode = true;
|
||||
continue;
|
||||
}
|
||||
else if (CuraApplication.checkIsValidProjectFile(fileUrls[i])) {
|
||||
projectFileUrlList.push(fileUrls[i]);
|
||||
}
|
||||
}
|
||||
|
||||
var meshName = backgroundItem.getMeshName(fileUrls[0].toString())
|
||||
backgroundItem.hasMesh(decodeURIComponent(meshName))
|
||||
// show a warning if selected multiple files together with Gcode
|
||||
var hasProjectFile = projectFileUrlList.length > 0;
|
||||
var selectedMultipleFiles = fileUrls.length > 1;
|
||||
if (selectedMultipleFiles && hasGcode) {
|
||||
infoMultipleFilesWithGcodeDialog.selectedMultipleFiles = selectedMultipleFiles;
|
||||
infoMultipleFilesWithGcodeDialog.hasProjectFile = hasProjectFile;
|
||||
infoMultipleFilesWithGcodeDialog.fileUrls = fileUrls;
|
||||
infoMultipleFilesWithGcodeDialog.projectFileUrlList = projectFileUrlList;
|
||||
infoMultipleFilesWithGcodeDialog.open();
|
||||
}
|
||||
else {
|
||||
handleOpenFiles(selectedMultipleFiles, hasProjectFile, fileUrls, projectFileUrlList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenFiles(selectedMultipleFiles, hasProjectFile, fileUrls, projectFileUrlList)
|
||||
{
|
||||
// we only allow opening one project file
|
||||
if (selectedMultipleFiles && hasProjectFile)
|
||||
{
|
||||
openFilesIncludingProjectsDialog.fileUrls = fileUrls;
|
||||
openFilesIncludingProjectsDialog.show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasProjectFile)
|
||||
{
|
||||
var projectFile = projectFileUrlList[0];
|
||||
|
||||
// check preference
|
||||
var choice = UM.Preferences.getValue("cura/choice_on_open_project");
|
||||
if (choice == "open_as_project")
|
||||
openFilesIncludingProjectsDialog.loadProjectFile(projectFile);
|
||||
else if (choice == "open_as_model")
|
||||
openFilesIncludingProjectsDialog.loadModelFiles([projectFile]);
|
||||
else // always ask
|
||||
{
|
||||
// ask whether to open as project or as models
|
||||
askOpenAsProjectOrModelsDialog.fileUrl = projectFile;
|
||||
askOpenAsProjectOrModelsDialog.show();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
openFilesIncludingProjectsDialog.loadModelFiles(fileUrls);
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: infoMultipleFilesWithGcodeDialog
|
||||
title: catalog.i18nc("@title:window", "Open File(s)")
|
||||
icon: StandardIcon.Information
|
||||
standardButtons: StandardButton.Ok
|
||||
text: catalog.i18nc("@text:window", "We have found one or more G-Code files within the files you have selected. You can only open one G-Code file at a time. If you want to open a G-Code file, please just select only one.")
|
||||
|
||||
property var selectedMultipleFiles
|
||||
property var hasProjectFile
|
||||
property var fileUrls
|
||||
property var projectFileUrlList
|
||||
|
||||
onAccepted:
|
||||
{
|
||||
handleOpenFiles(selectedMultipleFiles, hasProjectFile, fileUrls, projectFileUrlList);
|
||||
}
|
||||
}
|
||||
|
||||
@ -751,38 +842,14 @@ UM.MainWindow
|
||||
onTriggered: openDialog.open()
|
||||
}
|
||||
|
||||
FileDialog
|
||||
OpenFilesIncludingProjectsDialog
|
||||
{
|
||||
id: openWorkspaceDialog;
|
||||
|
||||
//: File open dialog title
|
||||
title: catalog.i18nc("@title:window","Open workspace")
|
||||
modality: UM.Application.platform == "linux" ? Qt.NonModal : Qt.WindowModal;
|
||||
selectMultiple: false
|
||||
nameFilters: UM.WorkspaceFileHandler.supportedReadFileTypes;
|
||||
folder: CuraApplication.getDefaultPath("dialog_load_path")
|
||||
onAccepted:
|
||||
{
|
||||
//Because several implementations of the file dialog only update the folder
|
||||
//when it is explicitly set.
|
||||
var f = folder;
|
||||
folder = f;
|
||||
|
||||
CuraApplication.setDefaultPath("dialog_load_path", folder);
|
||||
|
||||
for(var i in fileUrls)
|
||||
{
|
||||
UM.WorkspaceFileHandler.readLocalFile(fileUrls[i])
|
||||
}
|
||||
var meshName = backgroundItem.getMeshName(fileUrls[0].toString())
|
||||
backgroundItem.hasMesh(decodeURIComponent(meshName))
|
||||
}
|
||||
id: openFilesIncludingProjectsDialog
|
||||
}
|
||||
|
||||
Connections
|
||||
AskOpenAsProjectOrModelsDialog
|
||||
{
|
||||
target: Cura.Actions.loadWorkspace
|
||||
onTriggered: openWorkspaceDialog.open()
|
||||
id: askOpenAsProjectOrModelsDialog
|
||||
}
|
||||
|
||||
EngineLog
|
||||
@ -823,7 +890,7 @@ UM.MainWindow
|
||||
|
||||
function start(id)
|
||||
{
|
||||
var actions = Cura.MachineActionManager.getFirstStartActions(id)
|
||||
var actions = Cura.MachineActionManager.getFirstStartActions(id)
|
||||
resetPages() // Remove previous pages
|
||||
|
||||
for (var i = 0; i < actions.length; i++)
|
||||
@ -882,7 +949,6 @@ UM.MainWindow
|
||||
{
|
||||
discardOrKeepProfileChangesDialog.show()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Connections
|
||||
@ -932,4 +998,3 @@ UM.MainWindow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,9 +21,18 @@ UM.Dialog
|
||||
if(visible)
|
||||
{
|
||||
changesModel.forceUpdate()
|
||||
}
|
||||
|
||||
discardOrKeepProfileChangesDropDownButton.currentIndex = UM.Preferences.getValue("cura/choice_on_profile_override")
|
||||
discardOrKeepProfileChangesDropDownButton.currentIndex = 0;
|
||||
for (var i = 0; i < discardOrKeepProfileChangesDropDownButton.model.count; ++i)
|
||||
{
|
||||
var code = discardOrKeepProfileChangesDropDownButton.model.get(i).code;
|
||||
if (code == UM.Preferences.getValue("cura/choice_on_profile_override"))
|
||||
{
|
||||
discardOrKeepProfileChangesDropDownButton.currentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column
|
||||
@ -47,7 +56,7 @@ UM.Dialog
|
||||
|
||||
Label
|
||||
{
|
||||
text: "You have customized some profile settings.\nWould you like to keep or discard those settings?"
|
||||
text: catalog.i18nc("@text:window", "You have customized some profile settings.\nWould you like to keep or discard those settings?")
|
||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||
font: UM.Theme.getFont("default")
|
||||
wrapMode: Text.WordWrap
|
||||
@ -59,7 +68,7 @@ UM.Dialog
|
||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: base.height - 200
|
||||
height: base.height - 150
|
||||
id: tableView
|
||||
Component
|
||||
{
|
||||
@ -133,30 +142,35 @@ UM.Dialog
|
||||
ComboBox
|
||||
{
|
||||
id: discardOrKeepProfileChangesDropDownButton
|
||||
model: [
|
||||
catalog.i18nc("@option:discardOrKeep", "Always ask me this"),
|
||||
catalog.i18nc("@option:discardOrKeep", "Discard and never ask again"),
|
||||
catalog.i18nc("@option:discardOrKeep", "Keep and never ask again")
|
||||
]
|
||||
width: 300
|
||||
currentIndex: UM.Preferences.getValue("cura/choice_on_profile_override")
|
||||
onCurrentIndexChanged:
|
||||
|
||||
model: ListModel
|
||||
{
|
||||
UM.Preferences.setValue("cura/choice_on_profile_override", currentIndex)
|
||||
if (currentIndex == 1) {
|
||||
// 1 == "Discard and never ask again", so only enable the "Discard" button
|
||||
discardButton.enabled = true
|
||||
keepButton.enabled = false
|
||||
id: discardOrKeepProfileListModel
|
||||
|
||||
Component.onCompleted: {
|
||||
append({ text: catalog.i18nc("@option:discardOrKeep", "Always ask me this"), code: "always_ask" })
|
||||
append({ text: catalog.i18nc("@option:discardOrKeep", "Discard and never ask again"), code: "always_discard" })
|
||||
append({ text: catalog.i18nc("@option:discardOrKeep", "Keep and never ask again"), code: "always_keep" })
|
||||
}
|
||||
else if (currentIndex == 2) {
|
||||
// 2 == "Keep and never ask again", so only enable the "Keep" button
|
||||
keepButton.enabled = true
|
||||
discardButton.enabled = false
|
||||
}
|
||||
|
||||
onActivated:
|
||||
{
|
||||
var code = model.get(index).code;
|
||||
UM.Preferences.setValue("cura/choice_on_profile_override", code);
|
||||
|
||||
if (code == "always_keep") {
|
||||
keepButton.enabled = true;
|
||||
discardButton.enabled = false;
|
||||
}
|
||||
else if (code == "always_discard") {
|
||||
keepButton.enabled = false;
|
||||
discardButton.enabled = true;
|
||||
}
|
||||
else {
|
||||
// 0 == "Always ask me this", so show both
|
||||
keepButton.enabled = true
|
||||
discardButton.enabled = true
|
||||
keepButton.enabled = true;
|
||||
discardButton.enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -167,7 +181,7 @@ UM.Dialog
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||
height:childrenRect.height
|
||||
height: childrenRect.height
|
||||
|
||||
Button
|
||||
{
|
||||
|
@ -4,7 +4,7 @@
|
||||
import QtQuick 2.2
|
||||
import QtQuick.Controls 1.1
|
||||
|
||||
import UM 1.2 as UM
|
||||
import UM 1.3 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
||||
Menu
|
||||
@ -25,8 +25,38 @@ Menu
|
||||
var path = modelData.toString()
|
||||
return (index + 1) + ". " + path.slice(path.lastIndexOf("/") + 1);
|
||||
}
|
||||
onTriggered: {
|
||||
Printer.readLocalFile(modelData);
|
||||
onTriggered:
|
||||
{
|
||||
var toShowDialog = false;
|
||||
var toOpenAsProject = false;
|
||||
var toOpenAsModel = false;
|
||||
|
||||
if (CuraApplication.checkIsValidProjectFile(modelData)) {
|
||||
// check preference
|
||||
var choice = UM.Preferences.getValue("cura/choice_on_open_project");
|
||||
|
||||
if (choice == "open_as_project")
|
||||
toOpenAsProject = true;
|
||||
else if (choice == "open_as_model")
|
||||
toOpenAsModel = true;
|
||||
else
|
||||
toShowDialog = true;
|
||||
}
|
||||
else {
|
||||
toOpenAsModel = true;
|
||||
}
|
||||
|
||||
if (toShowDialog) {
|
||||
askOpenAsProjectOrModelsDialog.fileUrl = modelData;
|
||||
askOpenAsProjectOrModelsDialog.show();
|
||||
return;
|
||||
}
|
||||
|
||||
// open file in the prefered way
|
||||
if (toOpenAsProject)
|
||||
UM.WorkspaceFileHandler.readLocalFile(modelData);
|
||||
else if (toOpenAsModel)
|
||||
Printer.readLocalFile(modelData);
|
||||
var meshName = backgroundItem.getMeshName(modelData.toString())
|
||||
backgroundItem.hasMesh(decodeURIComponent(meshName))
|
||||
}
|
||||
@ -34,4 +64,9 @@ Menu
|
||||
onObjectAdded: menu.insertItem(index, object)
|
||||
onObjectRemoved: menu.removeItem(object)
|
||||
}
|
||||
|
||||
Cura.AskOpenAsProjectOrModelsDialog
|
||||
{
|
||||
id: askOpenAsProjectOrModelsDialog
|
||||
}
|
||||
}
|
||||
|
107
resources/qml/OpenFilesIncludingProjectsDialog.qml
Normal file
107
resources/qml/OpenFilesIncludingProjectsDialog.qml
Normal file
@ -0,0 +1,107 @@
|
||||
// Copyright (c) 2017 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.Controls.Styles 1.1
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Dialogs 1.1
|
||||
|
||||
import UM 1.3 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
||||
UM.Dialog
|
||||
{
|
||||
// This dialog asks the user whether he/she wants to open the project file we have detected or the model files.
|
||||
id: base
|
||||
|
||||
title: catalog.i18nc("@title:window", "Open file(s)")
|
||||
width: 420
|
||||
height: 170
|
||||
|
||||
maximumHeight: height
|
||||
maximumWidth: width
|
||||
minimumHeight: height
|
||||
minimumWidth: width
|
||||
|
||||
modality: UM.Application.platform == "linux" ? Qt.NonModal : Qt.WindowModal;
|
||||
|
||||
property var fileUrls: []
|
||||
property int spacerHeight: 10
|
||||
|
||||
function loadProjectFile(projectFile)
|
||||
{
|
||||
UM.WorkspaceFileHandler.readLocalFile(projectFile);
|
||||
|
||||
var meshName = backgroundItem.getMeshName(projectFile.toString());
|
||||
backgroundItem.hasMesh(decodeURIComponent(meshName));
|
||||
}
|
||||
|
||||
function loadModelFiles(fileUrls)
|
||||
{
|
||||
for (var i in fileUrls)
|
||||
Printer.readLocalFile(fileUrls[i]);
|
||||
|
||||
var meshName = backgroundItem.getMeshName(fileUrls[0].toString());
|
||||
backgroundItem.hasMesh(decodeURIComponent(meshName));
|
||||
}
|
||||
|
||||
Column
|
||||
{
|
||||
anchors.fill: parent
|
||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
|
||||
Text
|
||||
{
|
||||
text: catalog.i18nc("@text:window", "We have found multiple project file(s) within the files you have\nselected. You can open only one project file at a time. We\nsuggest to only import models from those files. Would you like\nto proceed?")
|
||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||
font: UM.Theme.getFont("default")
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Item // Spacer
|
||||
{
|
||||
height: base.spacerHeight
|
||||
width: height
|
||||
}
|
||||
|
||||
// Buttons
|
||||
Item
|
||||
{
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
height: childrenRect.height
|
||||
|
||||
Button
|
||||
{
|
||||
id: cancelButton
|
||||
text: catalog.i18nc("@action:button", "Cancel");
|
||||
anchors.right: importAllAsModelsButton.left
|
||||
anchors.rightMargin: UM.Theme.getSize("default_margin").width
|
||||
onClicked:
|
||||
{
|
||||
// cancel
|
||||
base.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Button
|
||||
{
|
||||
id: importAllAsModelsButton
|
||||
text: catalog.i18nc("@action:button", "Import all as models");
|
||||
anchors.right: parent.right
|
||||
isDefault: true
|
||||
onClicked:
|
||||
{
|
||||
// load models from all selected file
|
||||
loadModelFiles(base.fileUrls);
|
||||
|
||||
base.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -25,6 +25,30 @@ UM.PreferencesPage
|
||||
}
|
||||
}
|
||||
|
||||
function setDefaultDiscardOrKeepProfile(code)
|
||||
{
|
||||
for (var i = 0; i < choiceOnProfileOverrideDropDownButton.model.count; i++)
|
||||
{
|
||||
if (choiceOnProfileOverrideDropDownButton.model.get(i).code == code)
|
||||
{
|
||||
choiceOnProfileOverrideDropDownButton.currentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setDefaultOpenProjectOption(code)
|
||||
{
|
||||
for (var i = 0; i < choiceOnOpenProjectDropDownButton.model.count; ++i)
|
||||
{
|
||||
if (choiceOnOpenProjectDropDownButton.model.get(i).code == code)
|
||||
{
|
||||
choiceOnOpenProjectDropDownButton.currentIndex = i
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function reset()
|
||||
{
|
||||
UM.Preferences.resetPreference("general/language")
|
||||
@ -45,10 +69,16 @@ UM.PreferencesPage
|
||||
showOverhangCheckbox.checked = boolCheck(UM.Preferences.getValue("view/show_overhang"))
|
||||
UM.Preferences.resetPreference("view/center_on_select");
|
||||
centerOnSelectCheckbox.checked = boolCheck(UM.Preferences.getValue("view/center_on_select"))
|
||||
UM.Preferences.resetPreference("view/invert_zoom");
|
||||
invertZoomCheckbox.checked = boolCheck(UM.Preferences.getValue("view/invert_zoom"))
|
||||
UM.Preferences.resetPreference("view/top_layer_count");
|
||||
topLayerCountCheckbox.checked = boolCheck(UM.Preferences.getValue("view/top_layer_count"))
|
||||
|
||||
UM.Preferences.resetPreference("cura/choice_on_profile_override")
|
||||
choiceOnProfileOverrideDropDownButton.currentIndex = UM.Preferences.getValue("cura/choice_on_profile_override")
|
||||
setDefaultDiscardOrKeepProfile(UM.Preferences.getValue("cura/choice_on_profile_override"))
|
||||
|
||||
UM.Preferences.resetPreference("cura/choice_on_open_project")
|
||||
setDefaultOpenProjectOption(UM.Preferences.getValue("cura/choice_on_open_project"))
|
||||
|
||||
if (plugins.find("id", "SliceInfoPlugin") > -1) {
|
||||
UM.Preferences.resetPreference("info/send_slice_info")
|
||||
@ -229,6 +259,21 @@ UM.PreferencesPage
|
||||
text: catalog.i18nc("@action:button","Center camera when item is selected");
|
||||
checked: boolCheck(UM.Preferences.getValue("view/center_on_select"))
|
||||
onClicked: UM.Preferences.setValue("view/center_on_select", checked)
|
||||
enabled: Qt.platform.os != "windows" // Hack: disable the feature on windows as it's broken for pyqt 5.7.1.
|
||||
}
|
||||
}
|
||||
|
||||
UM.TooltipArea {
|
||||
width: childrenRect.width;
|
||||
height: childrenRect.height;
|
||||
text: catalog.i18nc("@info:tooltip","Should the default zoom behavior of cura be inverted?")
|
||||
|
||||
CheckBox
|
||||
{
|
||||
id: invertZoomCheckbox
|
||||
text: catalog.i18nc("@action:button","Invert the direction of camera zoom.");
|
||||
checked: boolCheck(UM.Preferences.getValue("view/invert_zoom"))
|
||||
onClicked: UM.Preferences.setValue("view/invert_zoom", checked)
|
||||
}
|
||||
}
|
||||
|
||||
@ -259,6 +304,25 @@ UM.PreferencesPage
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
UM.TooltipArea
|
||||
{
|
||||
width: childrenRect.width;
|
||||
height: childrenRect.height;
|
||||
|
||||
text: catalog.i18nc("@info:tooltip","Show caution message in gcode reader.")
|
||||
|
||||
CheckBox
|
||||
{
|
||||
id: gcodeShowCautionCheckbox
|
||||
|
||||
checked: boolCheck(UM.Preferences.getValue("gcodereader/show_caution"))
|
||||
onClicked: UM.Preferences.setValue("gcodereader/show_caution", checked)
|
||||
|
||||
text: catalog.i18nc("@option:check","Caution message in gcode reader");
|
||||
}
|
||||
}
|
||||
|
||||
UM.TooltipArea {
|
||||
width: childrenRect.width
|
||||
height: childrenRect.height
|
||||
@ -341,6 +405,56 @@ UM.PreferencesPage
|
||||
}
|
||||
}
|
||||
|
||||
UM.TooltipArea {
|
||||
width: childrenRect.width
|
||||
height: childrenRect.height
|
||||
text: catalog.i18nc("@info:tooltip", "Default behavior when opening a project file")
|
||||
|
||||
Column
|
||||
{
|
||||
spacing: 4
|
||||
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@window:text", "Default behavior when opening a project file: ")
|
||||
}
|
||||
|
||||
ComboBox
|
||||
{
|
||||
id: choiceOnOpenProjectDropDownButton
|
||||
width: 200
|
||||
|
||||
model: ListModel
|
||||
{
|
||||
id: openProjectOptionModel
|
||||
|
||||
Component.onCompleted: {
|
||||
append({ text: catalog.i18nc("@option:openProject", "Always ask"), code: "always_ask" })
|
||||
append({ text: catalog.i18nc("@option:openProject", "Always open as a project"), code: "open_as_project" })
|
||||
append({ text: catalog.i18nc("@option:openProject", "Always import models"), code: "open_as_model" })
|
||||
}
|
||||
}
|
||||
|
||||
currentIndex:
|
||||
{
|
||||
var index = 0;
|
||||
var currentChoice = UM.Preferences.getValue("cura/choice_on_open_project");
|
||||
for (var i = 0; i < model.count; ++i)
|
||||
{
|
||||
if (model.get(i).code == currentChoice)
|
||||
{
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
onActivated: UM.Preferences.setValue("cura/choice_on_open_project", model.get(index).code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item
|
||||
{
|
||||
//: Spacer
|
||||
@ -348,12 +462,6 @@ UM.PreferencesPage
|
||||
width: UM.Theme.getSize("default_margin").width
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
font.bold: true
|
||||
text: catalog.i18nc("@label", "Override Profile")
|
||||
}
|
||||
|
||||
UM.TooltipArea
|
||||
{
|
||||
width: childrenRect.width;
|
||||
@ -361,18 +469,48 @@ UM.PreferencesPage
|
||||
|
||||
text: catalog.i18nc("@info:tooltip", "When you have made changes to a profile and switched to a different one, a dialog will be shown asking whether you want to keep your modifications or not, or you can choose a default behaviour and never show that dialog again.")
|
||||
|
||||
ComboBox
|
||||
Column
|
||||
{
|
||||
id: choiceOnProfileOverrideDropDownButton
|
||||
spacing: 4
|
||||
|
||||
model: [
|
||||
catalog.i18nc("@option:discardOrKeep", "Always ask me this"),
|
||||
catalog.i18nc("@option:discardOrKeep", "Discard and never ask again"),
|
||||
catalog.i18nc("@option:discardOrKeep", "Keep and never ask again")
|
||||
]
|
||||
width: 300
|
||||
currentIndex: UM.Preferences.getValue("cura/choice_on_profile_override")
|
||||
onCurrentIndexChanged: UM.Preferences.setValue("cura/choice_on_profile_override", currentIndex)
|
||||
Label
|
||||
{
|
||||
font.bold: true
|
||||
text: catalog.i18nc("@label", "Override Profile")
|
||||
}
|
||||
|
||||
ComboBox
|
||||
{
|
||||
id: choiceOnProfileOverrideDropDownButton
|
||||
width: 200
|
||||
|
||||
model: ListModel
|
||||
{
|
||||
id: discardOrKeepProfileListModel
|
||||
|
||||
Component.onCompleted: {
|
||||
append({ text: catalog.i18nc("@option:discardOrKeep", "Always ask me this"), code: "always_ask" })
|
||||
append({ text: catalog.i18nc("@option:discardOrKeep", "Discard and never ask again"), code: "always_discard" })
|
||||
append({ text: catalog.i18nc("@option:discardOrKeep", "Keep and never ask again"), code: "always_keep" })
|
||||
}
|
||||
}
|
||||
|
||||
currentIndex:
|
||||
{
|
||||
var index = 0;
|
||||
var code = UM.Preferences.getValue("cura/choice_on_profile_override");
|
||||
for (var i = 0; i < model.count; ++i)
|
||||
{
|
||||
if (model.get(i).code == code)
|
||||
{
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
onActivated: UM.Preferences.setValue("cura/choice_on_profile_override", model.get(index).code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -153,7 +153,7 @@ TabView
|
||||
value: base.getMaterialPreferenceValue(properties.guid, "spool_cost")
|
||||
prefix: base.currency + " "
|
||||
decimals: 2
|
||||
maximumValue: 1000
|
||||
maximumValue: 100000000
|
||||
|
||||
onValueChanged: {
|
||||
base.setMaterialPreferenceValue(properties.guid, "spool_cost", parseFloat(value))
|
||||
|
@ -128,7 +128,11 @@ UM.ManagementPage
|
||||
text: catalog.i18nc("@action:button", "Activate");
|
||||
iconName: "list-activate";
|
||||
enabled: base.currentItem != null && base.currentItem.id != Cura.MachineManager.activeMaterialId && Cura.MachineManager.hasMaterials
|
||||
onClicked: Cura.MachineManager.setActiveMaterial(base.currentItem.id)
|
||||
onClicked:
|
||||
{
|
||||
Cura.MachineManager.setActiveMaterial(base.currentItem.id)
|
||||
currentItem = base.model.getItem(base.objectList.currentIndex) // Refresh the current item.
|
||||
}
|
||||
},
|
||||
Button
|
||||
{
|
||||
|
@ -76,6 +76,18 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// Shortcut for "save as/print/..."
|
||||
Action {
|
||||
shortcut: "Ctrl+P"
|
||||
onTriggered:
|
||||
{
|
||||
// only work when the button is enabled
|
||||
if (saveToButton.enabled) {
|
||||
saveToButton.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: saveRow
|
||||
width: base.width
|
||||
@ -215,7 +227,7 @@ Item {
|
||||
text: UM.OutputDeviceManager.activeDeviceShortDescription
|
||||
onClicked:
|
||||
{
|
||||
UM.OutputDeviceManager.requestWriteToDevice(UM.OutputDeviceManager.activeDevice, PrintInformation.jobName, { "filter_by_machine": true })
|
||||
UM.OutputDeviceManager.requestWriteToDevice(UM.OutputDeviceManager.activeDevice, PrintInformation.jobName, { "filter_by_machine": true, "preferred_mimetype":Printer.preferredOutputMimetype })
|
||||
}
|
||||
|
||||
style: ButtonStyle {
|
||||
|
@ -18,23 +18,10 @@ Item
|
||||
signal showTooltip(Item item, point location, string text);
|
||||
signal hideTooltip();
|
||||
|
||||
function toggleFilterField()
|
||||
{
|
||||
filterContainer.visible = !filterContainer.visible
|
||||
if(filterContainer.visible)
|
||||
{
|
||||
filter.forceActiveFocus();
|
||||
}
|
||||
else
|
||||
{
|
||||
filter.text = "";
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle
|
||||
{
|
||||
id: filterContainer
|
||||
visible: false
|
||||
visible: true
|
||||
|
||||
border.width: UM.Theme.getSize("default_lining").width
|
||||
border.color:
|
||||
@ -70,7 +57,7 @@ Item
|
||||
anchors.right: clearFilterButton.left
|
||||
anchors.rightMargin: UM.Theme.getSize("default_margin").width
|
||||
|
||||
placeholderText: catalog.i18nc("@label:textbox", "Filter...")
|
||||
placeholderText: catalog.i18nc("@label:textbox", "Search...")
|
||||
|
||||
style: TextFieldStyle
|
||||
{
|
||||
|
@ -362,7 +362,7 @@ Rectangle
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: model.index * (settingsModeSelection.width / 2)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 0.5 * parent.width - (model.showFilterButton ? toggleFilterButton.width : 0)
|
||||
width: 0.5 * parent.width
|
||||
text: model.text
|
||||
exclusiveGroup: modeMenuGroup;
|
||||
checkable: true;
|
||||
@ -418,44 +418,6 @@ Rectangle
|
||||
}
|
||||
}
|
||||
|
||||
Button
|
||||
{
|
||||
id: toggleFilterButton
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: UM.Theme.getSize("default_margin").width
|
||||
anchors.top: headerSeparator.bottom
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
|
||||
height: settingsModeSelection.height
|
||||
width: visible ? height : 0
|
||||
|
||||
visible: !monitoringPrint && !hideSettings && modesListModel.get(base.currentModeIndex) != undefined && modesListModel.get(base.currentModeIndex).showFilterButton
|
||||
opacity: visible ? 1 : 0
|
||||
|
||||
onClicked: sidebarContents.currentItem.toggleFilterField()
|
||||
|
||||
style: ButtonStyle
|
||||
{
|
||||
background: Rectangle
|
||||
{
|
||||
border.width: UM.Theme.getSize("default_lining").width
|
||||
border.color: UM.Theme.getColor("toggle_checked_border")
|
||||
color: visible ? UM.Theme.getColor("toggle_checked") : UM.Theme.getColor("toggle_hovered")
|
||||
Behavior on color { ColorAnimation { duration: 50; } }
|
||||
}
|
||||
label: UM.RecolorImage
|
||||
{
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: UM.Theme.getSize("default_margin").width / 2
|
||||
|
||||
source: UM.Theme.getIcon("search")
|
||||
color: UM.Theme.getColor("toggle_checked_text")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StackView
|
||||
{
|
||||
id: sidebarContents
|
||||
@ -570,14 +532,12 @@ Rectangle
|
||||
modesListModel.append({
|
||||
text: catalog.i18nc("@title:tab", "Recommended"),
|
||||
tooltipText: catalog.i18nc("@tooltip", "<b>Recommended Print Setup</b><br/><br/>Print with the recommended settings for the selected printer, material and quality."),
|
||||
item: sidebarSimple,
|
||||
showFilterButton: false
|
||||
item: sidebarSimple
|
||||
})
|
||||
modesListModel.append({
|
||||
text: catalog.i18nc("@title:tab", "Custom"),
|
||||
tooltipText: catalog.i18nc("@tooltip", "<b>Custom Print Setup</b><br/><br/>Print with finegrained control over every last bit of the slicing process."),
|
||||
item: sidebarAdvanced,
|
||||
showFilterButton: true
|
||||
item: sidebarAdvanced
|
||||
})
|
||||
sidebarContents.push({ "item": modesListModel.get(base.currentModeIndex).item, "immediate": true });
|
||||
|
||||
|
@ -8,6 +8,6 @@ weight = 0
|
||||
type = quality
|
||||
quality_type = normal
|
||||
material = generic_pva_ultimaker3_AA_0.4
|
||||
supported = false
|
||||
supported = False
|
||||
|
||||
[values]
|
||||
|
13
resources/quality/ultimaker3/um3_aa0.8_ABS_Not_Supported_Quality.inst.cfg
Executable file
13
resources/quality/ultimaker3/um3_aa0.8_ABS_Not_Supported_Quality.inst.cfg
Executable file
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = normal
|
||||
material = generic_abs_ultimaker3_AA_0.8
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = superdraft
|
||||
material = generic_abs_ultimaker3_AA_0.8
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
13
resources/quality/ultimaker3/um3_aa0.8_CPEP_Not_Supported_Quality.inst.cfg
Executable file
13
resources/quality/ultimaker3/um3_aa0.8_CPEP_Not_Supported_Quality.inst.cfg
Executable file
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = normal
|
||||
material = generic_cpe_plus_ultimaker3_AA_0.8
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = superdraft
|
||||
material = generic_cpe_plus_ultimaker3_AA_0.8
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
13
resources/quality/ultimaker3/um3_aa0.8_CPE_Not_Supported_Quality.inst.cfg
Executable file
13
resources/quality/ultimaker3/um3_aa0.8_CPE_Not_Supported_Quality.inst.cfg
Executable file
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = normal
|
||||
material = generic_cpe_ultimaker3_AA_0.8
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = superdraft
|
||||
material = generic_cpe_ultimaker3_AA_0.8
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
13
resources/quality/ultimaker3/um3_aa0.8_PC_Not_Supported_Quality.inst.cfg
Executable file
13
resources/quality/ultimaker3/um3_aa0.8_PC_Not_Supported_Quality.inst.cfg
Executable file
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
weight = 0
|
||||
type = quality
|
||||
quality_type = normal
|
||||
material = generic_pc_ultimaker3_AA_0.8
|
||||
supported = False
|
||||
|
||||
[values]
|
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
weight = 0
|
||||
type = quality
|
||||
quality_type = superdraft
|
||||
material = generic_pc_ultimaker3_AA_0.8
|
||||
supported = False
|
||||
|
||||
[values]
|
13
resources/quality/ultimaker3/um3_aa0.8_PVA_Not_Supported_Quality.inst.cfg
Executable file
13
resources/quality/ultimaker3/um3_aa0.8_PVA_Not_Supported_Quality.inst.cfg
Executable file
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
weight = 0
|
||||
type = quality
|
||||
quality_type = normal
|
||||
material = generic_pva_ultimaker3_AA_0.8
|
||||
supported = False
|
||||
|
||||
[values]
|
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
weight = 0
|
||||
type = quality
|
||||
quality_type = superdraft
|
||||
material = generic_pva_ultimaker3_AA_0.8
|
||||
supported = False
|
||||
|
||||
[values]
|
13
resources/quality/ultimaker3/um3_aa0.8_TPU_Not_Supported_Quality.inst.cfg
Executable file
13
resources/quality/ultimaker3/um3_aa0.8_TPU_Not_Supported_Quality.inst.cfg
Executable file
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
weight = 0
|
||||
type = quality
|
||||
quality_type = normal
|
||||
material = generic_tpu_ultimaker3_AA_0.8
|
||||
supported = False
|
||||
|
||||
[values]
|
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
weight = 0
|
||||
type = quality
|
||||
quality_type = superdraft
|
||||
material = generic_tpu_ultimaker3_AA_0.8
|
||||
supported = False
|
||||
|
||||
[values]
|
@ -8,6 +8,6 @@ type = quality
|
||||
quality_type = normal
|
||||
material = generic_abs_ultimaker3_BB_0.4
|
||||
weight = 0
|
||||
supported = false
|
||||
supported = False
|
||||
|
||||
[values]
|
||||
|
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = superdraft
|
||||
material = generic_abs_ultimaker3_BB_0.4
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
13
resources/quality/ultimaker3/um3_bb0.4_CPEP_Not_Supported_Quality.inst.cfg
Executable file
13
resources/quality/ultimaker3/um3_bb0.4_CPEP_Not_Supported_Quality.inst.cfg
Executable file
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = normal
|
||||
material = generic_cpe_plus_ultimaker3_BB_0.4
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = superdraft
|
||||
material = generic_cpe_plus_ultimaker3_BB_0.4
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
@ -8,6 +8,6 @@ type = quality
|
||||
quality_type = normal
|
||||
material = generic_cpe_ultimaker3_BB_0.4
|
||||
weight = 0
|
||||
supported = false
|
||||
supported = False
|
||||
|
||||
[values]
|
||||
|
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = superdraft
|
||||
material = generic_cpe_ultimaker3_BB_0.4
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
@ -8,6 +8,6 @@ type = quality
|
||||
quality_type = normal
|
||||
material = generic_nylon_ultimaker3_BB_0.4
|
||||
weight = 0
|
||||
supported = false
|
||||
supported = False
|
||||
|
||||
[values]
|
||||
|
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = superdraft
|
||||
material = generic_nylon_ultimaker3_BB_0.4
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
13
resources/quality/ultimaker3/um3_bb0.4_PC_Not_Supported_Quality.inst.cfg
Executable file
13
resources/quality/ultimaker3/um3_bb0.4_PC_Not_Supported_Quality.inst.cfg
Executable file
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = normal
|
||||
material = generic_pc_ultimaker3_BB_0.4
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
@ -8,6 +8,6 @@ type = quality
|
||||
quality_type = normal
|
||||
material = generic_pla_ultimaker3_BB_0.4
|
||||
weight = 0
|
||||
supported = false
|
||||
supported = False
|
||||
|
||||
[values]
|
||||
|
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = superdraft
|
||||
material = generic_pla_ultimaker3_BB_0.4
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
13
resources/quality/ultimaker3/um3_bb0.4_TPU_Not_Supported_Quality.inst.cfg
Executable file
13
resources/quality/ultimaker3/um3_bb0.4_TPU_Not_Supported_Quality.inst.cfg
Executable file
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = normal
|
||||
material = generic_tpu_ultimaker3_BB_0.4
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = superdraft
|
||||
material = generic_tpu_ultimaker3_BB_0.4
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
13
resources/quality/ultimaker3/um3_bb0.8_ABS_Not_Supported_Quality.inst.cfg
Executable file
13
resources/quality/ultimaker3/um3_bb0.8_ABS_Not_Supported_Quality.inst.cfg
Executable file
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = normal
|
||||
material = generic_abs_ultimaker3_BB_0.8
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = superdraft
|
||||
material = generic_abs_ultimaker3_BB_0.8
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
13
resources/quality/ultimaker3/um3_bb0.8_CPEP_Not_Supported_Quality.inst.cfg
Executable file
13
resources/quality/ultimaker3/um3_bb0.8_CPEP_Not_Supported_Quality.inst.cfg
Executable file
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = normal
|
||||
material = generic_cpe_plus_ultimaker3_BB_0.8
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = superdraft
|
||||
material = generic_cpe_plus_ultimaker3_BB_0.8
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
13
resources/quality/ultimaker3/um3_bb0.8_CPE_Not_Supported_Quality.inst.cfg
Executable file
13
resources/quality/ultimaker3/um3_bb0.8_CPE_Not_Supported_Quality.inst.cfg
Executable file
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = normal
|
||||
material = generic_cpe_ultimaker3_BB_0.8
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = superdraft
|
||||
material = generic_cpe_ultimaker3_BB_0.8
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
13
resources/quality/ultimaker3/um3_bb0.8_Nylon_Not_Supported_Quality.inst.cfg
Executable file
13
resources/quality/ultimaker3/um3_bb0.8_Nylon_Not_Supported_Quality.inst.cfg
Executable file
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = normal
|
||||
material = generic_nylon_ultimaker3_BB_0.8
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = superdraft
|
||||
material = generic_nylon_ultimaker3_BB_0.8
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
13
resources/quality/ultimaker3/um3_bb0.8_PC_Not_Supported_Quality.inst.cfg
Executable file
13
resources/quality/ultimaker3/um3_bb0.8_PC_Not_Supported_Quality.inst.cfg
Executable file
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = normal
|
||||
material = generic_pc_ultimaker3_BB_0.8
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = superdraft
|
||||
material = generic_pc_ultimaker3_BB_0.8
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
13
resources/quality/ultimaker3/um3_bb0.8_PLA_Not_Supported_Quality.inst.cfg
Executable file
13
resources/quality/ultimaker3/um3_bb0.8_PLA_Not_Supported_Quality.inst.cfg
Executable file
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = normal
|
||||
material = generic_pla_ultimaker3_BB_0.8
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = superdraft
|
||||
material = generic_pla_ultimaker3_BB_0.8
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
13
resources/quality/ultimaker3/um3_bb0.8_TPU_Not_Supported_Quality.inst.cfg
Executable file
13
resources/quality/ultimaker3/um3_bb0.8_TPU_Not_Supported_Quality.inst.cfg
Executable file
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = normal
|
||||
material = generic_tpu_ultimaker3_BB_0.8
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version = 2
|
||||
name = Not Supported
|
||||
definition = ultimaker3
|
||||
|
||||
[metadata]
|
||||
type = quality
|
||||
quality_type = superdraft
|
||||
material = generic_tpu_ultimaker3_BB_0.8
|
||||
weight = 0
|
||||
supported = False
|
||||
|
||||
[values]
|
12
tests/TestMachineAction.py
Normal file → Executable file
12
tests/TestMachineAction.py
Normal file → Executable file
@ -30,19 +30,19 @@ def test_addMachineAction():
|
||||
machine_manager.addMachineAction(test_action)
|
||||
|
||||
# Check that the machine has no supported actions yet.
|
||||
assert machine_manager.getSupportedActions(test_machine) == set()
|
||||
assert machine_manager.getSupportedActions(test_machine) == list()
|
||||
|
||||
# Check if adding a supported action works.
|
||||
machine_manager.addSupportedAction(test_machine, "test_action")
|
||||
assert machine_manager.getSupportedActions(test_machine) == {test_action}
|
||||
assert machine_manager.getSupportedActions(test_machine) == [test_action, ]
|
||||
|
||||
# Check that adding a unknown action doesn't change anything.
|
||||
machine_manager.addSupportedAction(test_machine, "key_that_doesnt_exist")
|
||||
assert machine_manager.getSupportedActions(test_machine) == {test_action}
|
||||
assert machine_manager.getSupportedActions(test_machine) == [test_action, ]
|
||||
|
||||
# Check if adding multiple supported actions works.
|
||||
machine_manager.addSupportedAction(test_machine, "test_action_2")
|
||||
assert machine_manager.getSupportedActions(test_machine) == {test_action, test_action_2}
|
||||
assert machine_manager.getSupportedActions(test_machine) == [test_action, test_action_2]
|
||||
|
||||
# Check that the machine has no required actions yet.
|
||||
assert machine_manager.getRequiredActions(test_machine) == set()
|
||||
@ -53,11 +53,11 @@ def test_addMachineAction():
|
||||
|
||||
## Check if adding single required action works
|
||||
machine_manager.addRequiredAction(test_machine, "test_action")
|
||||
assert machine_manager.getRequiredActions(test_machine) == {test_action}
|
||||
assert machine_manager.getRequiredActions(test_machine) == [test_action, ]
|
||||
|
||||
# Check if adding multiple required actions works.
|
||||
machine_manager.addRequiredAction(test_machine, "test_action_2")
|
||||
assert machine_manager.getRequiredActions(test_machine) == {test_action, test_action_2}
|
||||
assert machine_manager.getRequiredActions(test_machine) == [test_action, test_action_2]
|
||||
|
||||
# Ensure that firstStart actions are empty by default.
|
||||
assert machine_manager.getFirstStartActions(test_machine) == []
|
||||
|
Loading…
x
Reference in New Issue
Block a user