Merge branch 'master' into WIP_improve_initialization

Conflicts:
	cura/AutoSave.py
	cura/BuildVolume.py
	cura/CuraApplication.py

Contributes to CURA-5164
This commit is contained in:
Diego Prado Gesto 2018-05-25 09:39:51 +02:00
commit 5704a7b184
41 changed files with 1109 additions and 341 deletions

1
.gitignore vendored
View file

@ -40,6 +40,7 @@ plugins/cura-siemensnx-plugin
plugins/CuraBlenderPlugin plugins/CuraBlenderPlugin
plugins/CuraCloudPlugin plugins/CuraCloudPlugin
plugins/CuraDrivePlugin plugins/CuraDrivePlugin
plugins/CuraDrive
plugins/CuraLiveScriptingPlugin plugins/CuraLiveScriptingPlugin
plugins/CuraOpenSCADPlugin plugins/CuraOpenSCADPlugin
plugins/CuraPrintProfileCreator plugins/CuraPrintProfileCreator

32
cura/API/Backups.py Normal file
View file

@ -0,0 +1,32 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from cura.Backups.BackupsManager import BackupsManager
class Backups:
"""
The backups API provides a version-proof bridge between Cura's BackupManager and plugins that hook into it.
Usage:
from cura.API import CuraAPI
api = CuraAPI()
api.backups.createBackup()
api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})
"""
manager = BackupsManager() # Re-used instance of the backups manager.
def createBackup(self) -> (bytes, dict):
"""
Create a new backup using the BackupsManager.
:return: Tuple containing a ZIP file with the backup data and a dict with meta data about the backup.
"""
return self.manager.createBackup()
def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None:
"""
Restore a backup using the BackupManager.
:param zip_file: A ZIP file containing the actual backup data.
:param meta_data: Some meta data needed for restoring a backup, like the Cura version number.
"""
return self.manager.restoreBackup(zip_file, meta_data)

19
cura/API/__init__.py Normal file
View file

@ -0,0 +1,19 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.PluginRegistry import PluginRegistry
from cura.API.Backups import Backups
class CuraAPI:
"""
The official Cura API that plugins can use to interact with Cura.
Python does not technically prevent talking to other classes as well,
but this API provides a version-safe interface with proper deprecation warnings etc.
Usage of any other methods than the ones provided in this API can cause plugins to be unstable.
"""
# For now we use the same API version to be consistent.
VERSION = PluginRegistry.APIVersion
# Backups API.
backups = Backups()

View file

@ -1,3 +1,6 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Logger import Logger from UM.Logger import Logger
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
@ -18,17 +21,20 @@ LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points
# good locations for objects that you try to put on a build place. # good locations for objects that you try to put on a build place.
# Different priority schemes can be defined so it alters the behavior while using # Different priority schemes can be defined so it alters the behavior while using
# the same logic. # the same logic.
#
# Note: Make sure the scale is the same between ShapeArray objects and the Arrange instance.
class Arrange: class Arrange:
build_volume = None build_volume = None
def __init__(self, x, y, offset_x, offset_y, scale= 1.0): def __init__(self, x, y, offset_x, offset_y, scale= 0.5):
self.shape = (y, x)
self._priority = numpy.zeros((x, y), dtype=numpy.int32)
self._priority_unique_values = []
self._occupied = numpy.zeros((x, y), dtype=numpy.int32)
self._scale = scale # convert input coordinates to arrange coordinates self._scale = scale # convert input coordinates to arrange coordinates
self._offset_x = offset_x world_x, world_y = int(x * self._scale), int(y * self._scale)
self._offset_y = offset_y self._shape = (world_y, world_x)
self._priority = numpy.zeros((world_y, world_x), dtype=numpy.int32) # beware: these are indexed (y, x)
self._priority_unique_values = []
self._occupied = numpy.zeros((world_y, world_x), dtype=numpy.int32) # beware: these are indexed (y, x)
self._offset_x = int(offset_x * self._scale)
self._offset_y = int(offset_y * self._scale)
self._last_priority = 0 self._last_priority = 0
self._is_empty = True self._is_empty = True
@ -39,7 +45,7 @@ class Arrange:
# \param scene_root Root for finding all scene nodes # \param scene_root Root for finding all scene nodes
# \param fixed_nodes Scene nodes to be placed # \param fixed_nodes Scene nodes to be placed
@classmethod @classmethod
def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 220, y = 220): def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250):
arranger = Arrange(x, y, x // 2, y // 2, scale = scale) arranger = Arrange(x, y, x // 2, y // 2, scale = scale)
arranger.centerFirst() arranger.centerFirst()
@ -61,13 +67,17 @@ class Arrange:
# If a build volume was set, add the disallowed areas # If a build volume was set, add the disallowed areas
if Arrange.build_volume: if Arrange.build_volume:
disallowed_areas = Arrange.build_volume.getDisallowedAreas() disallowed_areas = Arrange.build_volume.getDisallowedAreasNoBrim()
for area in disallowed_areas: for area in disallowed_areas:
points = copy.deepcopy(area._points) points = copy.deepcopy(area._points)
shape_arr = ShapeArray.fromPolygon(points, scale = scale) shape_arr = ShapeArray.fromPolygon(points, scale = scale)
arranger.place(0, 0, shape_arr, update_empty = False) arranger.place(0, 0, shape_arr, update_empty = False)
return arranger return arranger
## This resets the optimization for finding location based on size
def resetLastPriority(self):
self._last_priority = 0
## Find placement for a node (using offset shape) and place it (using hull shape) ## Find placement for a node (using offset shape) and place it (using hull shape)
# return the nodes that should be placed # return the nodes that should be placed
# \param node # \param node
@ -104,7 +114,7 @@ class Arrange:
def centerFirst(self): def centerFirst(self):
# Square distance: creates a more round shape # Square distance: creates a more round shape
self._priority = numpy.fromfunction( self._priority = numpy.fromfunction(
lambda i, j: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self.shape, dtype=numpy.int32) lambda j, i: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self._shape, dtype=numpy.int32)
self._priority_unique_values = numpy.unique(self._priority) self._priority_unique_values = numpy.unique(self._priority)
self._priority_unique_values.sort() self._priority_unique_values.sort()
@ -112,7 +122,7 @@ class Arrange:
# This is a strategy for the arranger. # This is a strategy for the arranger.
def backFirst(self): def backFirst(self):
self._priority = numpy.fromfunction( self._priority = numpy.fromfunction(
lambda i, j: 10 * j + abs(self._offset_x - i), self.shape, dtype=numpy.int32) lambda j, i: 10 * j + abs(self._offset_x - i), self._shape, dtype=numpy.int32)
self._priority_unique_values = numpy.unique(self._priority) self._priority_unique_values = numpy.unique(self._priority)
self._priority_unique_values.sort() self._priority_unique_values.sort()
@ -126,9 +136,15 @@ class Arrange:
y = int(self._scale * y) y = int(self._scale * y)
offset_x = x + self._offset_x + shape_arr.offset_x offset_x = x + self._offset_x + shape_arr.offset_x
offset_y = y + self._offset_y + shape_arr.offset_y offset_y = y + self._offset_y + shape_arr.offset_y
if offset_x < 0 or offset_y < 0:
return None # out of bounds in self._occupied
occupied_x_max = offset_x + shape_arr.arr.shape[1]
occupied_y_max = offset_y + shape_arr.arr.shape[0]
if occupied_x_max > self._occupied.shape[1] + 1 or occupied_y_max > self._occupied.shape[0] + 1:
return None # out of bounds in self._occupied
occupied_slice = self._occupied[ occupied_slice = self._occupied[
offset_y:offset_y + shape_arr.arr.shape[0], offset_y:occupied_y_max,
offset_x:offset_x + shape_arr.arr.shape[1]] offset_x:occupied_x_max]
try: try:
if numpy.any(occupied_slice[numpy.where(shape_arr.arr == 1)]): if numpy.any(occupied_slice[numpy.where(shape_arr.arr == 1)]):
return None return None
@ -140,7 +156,7 @@ class Arrange:
return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)]) return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)])
## Find "best" spot for ShapeArray ## Find "best" spot for ShapeArray
# Return namedtuple with properties x, y, penalty_points, priority # Return namedtuple with properties x, y, penalty_points, priority.
# \param shape_arr ShapeArray # \param shape_arr ShapeArray
# \param start_prio Start with this priority value (and skip the ones before) # \param start_prio Start with this priority value (and skip the ones before)
# \param step Slicing value, higher = more skips = faster but less accurate # \param step Slicing value, higher = more skips = faster but less accurate
@ -153,12 +169,11 @@ class Arrange:
for priority in self._priority_unique_values[start_idx::step]: for priority in self._priority_unique_values[start_idx::step]:
tryout_idx = numpy.where(self._priority == priority) tryout_idx = numpy.where(self._priority == priority)
for idx in range(len(tryout_idx[0])): for idx in range(len(tryout_idx[0])):
x = tryout_idx[0][idx] x = tryout_idx[1][idx]
y = tryout_idx[1][idx] y = tryout_idx[0][idx]
projected_x = x - self._offset_x projected_x = int((x - self._offset_x) / self._scale)
projected_y = y - self._offset_y projected_y = int((y - self._offset_y) / self._scale)
# array to "world" coordinates
penalty_points = self.checkShape(projected_x, projected_y, shape_arr) penalty_points = self.checkShape(projected_x, projected_y, shape_arr)
if penalty_points is not None: if penalty_points is not None:
return LocationSuggestion(x = projected_x, y = projected_y, penalty_points = penalty_points, priority = priority) return LocationSuggestion(x = projected_x, y = projected_y, penalty_points = penalty_points, priority = priority)
@ -191,8 +206,12 @@ class Arrange:
# Set priority to low (= high number), so it won't get picked at trying out. # Set priority to low (= high number), so it won't get picked at trying out.
prio_slice = self._priority[min_y:max_y, min_x:max_x] prio_slice = self._priority[min_y:max_y, min_x:max_x]
prio_slice[numpy.where(shape_arr.arr[ prio_slice[new_occupied] = 999
min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 999
# If you want to see how the rasterized arranger build plate looks like, uncomment this code
# numpy.set_printoptions(linewidth=500, edgeitems=200)
# print(self._occupied.shape)
# print(self._occupied)
@property @property
def isEmpty(self): def isEmpty(self):

View file

@ -1,6 +1,7 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from UM.Job import Job from UM.Job import Job
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
@ -17,6 +18,7 @@ from cura.Arranging.ShapeArray import ShapeArray
from typing import List from typing import List
## Do arrangements on multiple build plates (aka builtiplexer)
class ArrangeArray: class ArrangeArray:
def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]): def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]):
self._x = x self._x = x
@ -79,7 +81,11 @@ class ArrangeObjectsAllBuildPlatesJob(Job):
nodes_arr.sort(key=lambda item: item[0]) nodes_arr.sort(key=lambda item: item[0])
nodes_arr.reverse() nodes_arr.reverse()
x, y = 200, 200 global_container_stack = Application.getInstance().getGlobalContainerStack()
machine_width = global_container_stack.getProperty("machine_width", "value")
machine_depth = global_container_stack.getProperty("machine_depth", "value")
x, y = machine_width, machine_depth
arrange_array = ArrangeArray(x = x, y = y, fixed_nodes = []) arrange_array = ArrangeArray(x = x, y = y, fixed_nodes = [])
arrange_array.add() arrange_array.add()
@ -93,27 +99,18 @@ class ArrangeObjectsAllBuildPlatesJob(Job):
for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr): for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr):
# For performance reasons, we assume that when a location does not fit, # For performance reasons, we assume that when a location does not fit,
# it will also not fit for the next object (while what can be untrue). # it will also not fit for the next object (while what can be untrue).
# We also skip possibilities by slicing through the possibilities (step = 10)
try_placement = True try_placement = True
current_build_plate_number = 0 # always start with the first one current_build_plate_number = 0 # always start with the first one
# # Only for first build plate
# if last_size == size and last_build_plate_number == current_build_plate_number:
# # This optimization works if many of the objects have the same size
# # Continue with same build plate number
# start_priority = last_priority
# else:
# start_priority = 0
while try_placement: while try_placement:
# make sure that current_build_plate_number is not going crazy or you'll have a lot of arrange objects # make sure that current_build_plate_number is not going crazy or you'll have a lot of arrange objects
while current_build_plate_number >= arrange_array.count(): while current_build_plate_number >= arrange_array.count():
arrange_array.add() arrange_array.add()
arranger = arrange_array.get(current_build_plate_number) arranger = arrange_array.get(current_build_plate_number)
best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority, step=10) best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority)
x, y = best_spot.x, best_spot.y x, y = best_spot.x, best_spot.y
node.removeDecorator(ZOffsetDecorator) node.removeDecorator(ZOffsetDecorator)
if node.getBoundingBox(): if node.getBoundingBox():

View file

@ -1,6 +1,7 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from UM.Job import Job from UM.Job import Job
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
@ -32,7 +33,11 @@ class ArrangeObjectsJob(Job):
progress = 0, progress = 0,
title = i18n_catalog.i18nc("@info:title", "Finding Location")) title = i18n_catalog.i18nc("@info:title", "Finding Location"))
status_message.show() status_message.show()
arranger = Arrange.create(fixed_nodes = self._fixed_nodes) global_container_stack = Application.getInstance().getGlobalContainerStack()
machine_width = global_container_stack.getProperty("machine_width", "value")
machine_depth = global_container_stack.getProperty("machine_depth", "value")
arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = self._fixed_nodes)
# Collect nodes to be placed # Collect nodes to be placed
nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr) nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr)
@ -50,15 +55,15 @@ class ArrangeObjectsJob(Job):
last_size = None last_size = None
grouped_operation = GroupedOperation() grouped_operation = GroupedOperation()
found_solution_for_all = True found_solution_for_all = True
not_fit_count = 0
for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr): for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr):
# For performance reasons, we assume that when a location does not fit, # For performance reasons, we assume that when a location does not fit,
# it will also not fit for the next object (while what can be untrue). # it will also not fit for the next object (while what can be untrue).
# We also skip possibilities by slicing through the possibilities (step = 10)
if last_size == size: # This optimization works if many of the objects have the same size if last_size == size: # This optimization works if many of the objects have the same size
start_priority = last_priority start_priority = last_priority
else: else:
start_priority = 0 start_priority = 0
best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority, step=10) best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority)
x, y = best_spot.x, best_spot.y x, y = best_spot.x, best_spot.y
node.removeDecorator(ZOffsetDecorator) node.removeDecorator(ZOffsetDecorator)
if node.getBoundingBox(): if node.getBoundingBox():
@ -70,12 +75,12 @@ class ArrangeObjectsJob(Job):
last_priority = best_spot.priority last_priority = best_spot.priority
arranger.place(x, y, hull_shape_arr) # take place before the next one arranger.place(x, y, hull_shape_arr) # take place before the next one
grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True)) grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))
else: else:
Logger.log("d", "Arrange all: could not find spot!") Logger.log("d", "Arrange all: could not find spot!")
found_solution_for_all = False found_solution_for_all = False
grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, - idx * 20), set_position = True)) grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, -not_fit_count * 20), set_position = True))
not_fit_count += 1
status_message.setProgress((idx + 1) / len(nodes_arr) * 100) status_message.setProgress((idx + 1) / len(nodes_arr) * 100)
Job.yieldThread() Job.yieldThread()

View file

@ -74,7 +74,7 @@ class ShapeArray:
# \param vertices # \param vertices
@classmethod @classmethod
def arrayFromPolygon(cls, shape, vertices): def arrayFromPolygon(cls, shape, vertices):
base_array = numpy.zeros(shape, dtype=float) # Initialize your array of zeros base_array = numpy.zeros(shape, dtype = numpy.int32) # Initialize your array of zeros
fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill

52
cura/AutoSave.py Normal file
View file

@ -0,0 +1,52 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QTimer
from UM.Logger import Logger
class AutoSave:
def __init__(self, application):
self._application = application
self._application.getPreferences().preferenceChanged.connect(self._triggerTimer)
self._global_stack = None
self._application.getPreferences().addPreference("cura/autosave_delay", 1000 * 10)
self._change_timer = QTimer()
self._change_timer.setInterval(self._application.getPreferences().getValue("cura/autosave_delay"))
self._change_timer.setSingleShot(True)
self._saving = False
def initialize(self):
# only initialise if the application is created and has started
self._change_timer.timeout.connect(self._onTimeout)
self._application.globalContainerStackChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
self._triggerTimer()
def _triggerTimer(self, *args):
if not self._saving:
self._change_timer.start()
def _onGlobalStackChanged(self):
if self._global_stack:
self._global_stack.propertyChanged.disconnect(self._triggerTimer)
self._global_stack.containersChanged.disconnect(self._triggerTimer)
self._global_stack = self._application.getGlobalContainerStack()
if self._global_stack:
self._global_stack.propertyChanged.connect(self._triggerTimer)
self._global_stack.containersChanged.connect(self._triggerTimer)
def _onTimeout(self):
self._saving = True # To prevent the save process from triggering another autosave.
Logger.log("d", "Autosaving preferences, instances and profiles")
self._application.saveSettings()
self._saving = False

155
cura/Backups/Backup.py Normal file
View file

@ -0,0 +1,155 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import io
import os
import re
import shutil
from typing import Optional
from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile
from UM import i18nCatalog
from UM.Logger import Logger
from UM.Message import Message
from UM.Platform import Platform
from UM.Resources import Resources
from cura.CuraApplication import CuraApplication
class Backup:
"""
The backup class holds all data about a backup.
It is also responsible for reading and writing the zip file to the user data folder.
"""
# These files should be ignored when making a backup.
IGNORED_FILES = [r"cura\.log", r"plugins\.json", r"cache", r"__pycache__", r"\.qmlc", r"\.pyc"]
# Re-use translation catalog.
catalog = i18nCatalog("cura")
def __init__(self, zip_file: bytes = None, meta_data: dict = None):
self.zip_file = zip_file # type: Optional[bytes]
self.meta_data = meta_data # type: Optional[dict]
def makeFromCurrent(self) -> (bool, Optional[str]):
"""
Create a backup from the current user config folder.
"""
cura_release = CuraApplication.getInstance().getVersion()
version_data_dir = Resources.getDataStoragePath()
Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir)
# Ensure all current settings are saved.
CuraApplication.getInstance().saveSettings()
# We copy the preferences file to the user data directory in Linux as it's in a different location there.
# When restoring a backup on Linux, we move it back.
if Platform.isLinux():
preferences_file_name = CuraApplication.getInstance().getApplicationName()
preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file)
shutil.copyfile(preferences_file, backup_preferences_file)
# Create an empty buffer and write the archive to it.
buffer = io.BytesIO()
archive = self._makeArchive(buffer, version_data_dir)
files = archive.namelist()
# Count the metadata items. We do this in a rather naive way at the moment.
machine_count = len([s for s in files if "machine_instances/" in s]) - 1
material_count = len([s for s in files if "materials/" in s]) - 1
profile_count = len([s for s in files if "quality_changes/" in s]) - 1
plugin_count = len([s for s in files if "plugin.json" in s])
# Store the archive and metadata so the BackupManager can fetch them when needed.
self.zip_file = buffer.getvalue()
self.meta_data = {
"cura_release": cura_release,
"machine_count": str(machine_count),
"material_count": str(material_count),
"profile_count": str(profile_count),
"plugin_count": str(plugin_count)
}
def _makeArchive(self, buffer: "io.BytesIO", root_path: str) -> Optional[ZipFile]:
"""
Make a full archive from the given root path with the given name.
:param root_path: The root directory to archive recursively.
:return: The archive as bytes.
"""
ignore_string = re.compile("|".join(self.IGNORED_FILES))
try:
archive = ZipFile(buffer, "w", ZIP_DEFLATED)
for root, folders, files in os.walk(root_path):
for item_name in folders + files:
absolute_path = os.path.join(root, item_name)
if ignore_string.search(absolute_path):
continue
archive.write(absolute_path, absolute_path[len(root_path) + len(os.sep):])
archive.close()
return archive
except (IOError, OSError, BadZipfile) as error:
Logger.log("e", "Could not create archive from user data directory: %s", error)
self._showMessage(
self.catalog.i18nc("@info:backup_failed",
"Could not create archive from user data directory: {}".format(error)))
return None
def _showMessage(self, message: str) -> None:
"""Show a UI message"""
Message(message, title=self.catalog.i18nc("@info:title", "Backup"), lifetime=30).show()
def restore(self) -> bool:
"""
Restore this backups
:return: A boolean whether we had success or not.
"""
if not self.zip_file or not self.meta_data or not self.meta_data.get("cura_release", None):
# We can restore without the minimum required information.
Logger.log("w", "Tried to restore a Cura backup without having proper data or meta data.")
self._showMessage(
self.catalog.i18nc("@info:backup_failed",
"Tried to restore a Cura backup without having proper data or meta data."))
return False
current_version = CuraApplication.getInstance().getVersion()
version_to_restore = self.meta_data.get("cura_release", "master")
if current_version != version_to_restore:
# Cannot restore version older or newer than current because settings might have changed.
# Restoring this will cause a lot of issues so we don't allow this for now.
self._showMessage(
self.catalog.i18nc("@info:backup_failed",
"Tried to restore a Cura backup that does not match your current version."))
return False
version_data_dir = Resources.getDataStoragePath()
archive = ZipFile(io.BytesIO(self.zip_file), "r")
extracted = self._extractArchive(archive, version_data_dir)
# Under Linux, preferences are stored elsewhere, so we copy the file to there.
if Platform.isLinux():
preferences_file_name = CuraApplication.getInstance().getApplicationName()
preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
Logger.log("d", "Moving preferences file from %s to %s", backup_preferences_file, preferences_file)
shutil.move(backup_preferences_file, preferences_file)
return extracted
@staticmethod
def _extractArchive(archive: "ZipFile", target_path: str) -> bool:
"""
Extract the whole archive to the given target path.
:param archive: The archive as ZipFile.
:param target_path: The target path.
:return: A boolean whether we had success or not.
"""
Logger.log("d", "Removing current data in location: %s", target_path)
Resources.factoryReset()
Logger.log("d", "Extracting backup to location: %s", target_path)
archive.extractall(target_path)
return True

View file

@ -0,0 +1,56 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
from UM.Logger import Logger
from cura.Backups.Backup import Backup
from cura.CuraApplication import CuraApplication
class BackupsManager:
"""
The BackupsManager is responsible for managing the creating and restoring of backups.
Backups themselves are represented in a different class.
"""
def __init__(self):
self._application = CuraApplication.getInstance()
def createBackup(self) -> (Optional[bytes], Optional[dict]):
"""
Get a backup of the current configuration.
:return: A Tuple containing a ZipFile (the actual backup) and a dict containing some meta data (like version).
"""
self._disableAutoSave()
backup = Backup()
backup.makeFromCurrent()
self._enableAutoSave()
# We don't return a Backup here because we want plugins only to interact with our API and not full objects.
return backup.zip_file, backup.meta_data
def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None:
"""
Restore a backup from a given ZipFile.
:param zip_file: A bytes object containing the actual backup.
:param meta_data: A dict containing some meta data that is needed to restore the backup correctly.
"""
if not meta_data.get("cura_release", None):
# If there is no "cura_release" specified in the meta data, we don't execute a backup restore.
Logger.log("w", "Tried to restore a backup without specifying a Cura version number.")
return
self._disableAutoSave()
backup = Backup(zip_file = zip_file, meta_data = meta_data)
restored = backup.restore()
if restored:
# At this point, Cura will need to restart for the changes to take effect.
# We don't want to store the data at this point as that would override the just-restored backup.
self._application.windowClosed(save_data=False)
def _disableAutoSave(self):
"""Here we try to disable the auto-save plugin as it might interfere with restoring a backup."""
self._application.setSaveDataEnabled(False)
def _enableAutoSave(self):
"""Re-enable auto-save after we're done."""
self._application.setSaveDataEnabled(True)

0
cura/Backups/__init__.py Normal file
View file

View file

@ -1,12 +1,8 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import math
from typing import List, Optional
import numpy
from PyQt5.QtCore import QTimer
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Settings.ExtruderManager import ExtruderManager
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Scene.Platform import Platform from UM.Scene.Platform import Platform
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
@ -20,14 +16,17 @@ from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Polygon import Polygon from UM.Math.Polygon import Polygon
from UM.Message import Message from UM.Message import Message
from UM.Signal import Signal from UM.Signal import Signal
from PyQt5.QtCore import QTimer
from UM.View.RenderBatch import RenderBatch from UM.View.RenderBatch import RenderBatch
from UM.View.GL.OpenGL import OpenGL from UM.View.GL.OpenGL import OpenGL
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Settings.ExtruderManager import ExtruderManager
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
import numpy
import math
import copy
from typing import List, Optional
# Setting for clearance around the prime # Setting for clearance around the prime
PRIME_CLEARANCE = 6.5 PRIME_CLEARANCE = 6.5
@ -63,6 +62,7 @@ class BuildVolume(SceneNode):
self._grid_shader = None self._grid_shader = None
self._disallowed_areas = [] self._disallowed_areas = []
self._disallowed_areas_no_brim = []
self._disallowed_area_mesh = None self._disallowed_area_mesh = None
self._error_areas = [] self._error_areas = []
@ -173,6 +173,9 @@ class BuildVolume(SceneNode):
def getDisallowedAreas(self) -> List[Polygon]: def getDisallowedAreas(self) -> List[Polygon]:
return self._disallowed_areas return self._disallowed_areas
def getDisallowedAreasNoBrim(self) -> List[Polygon]:
return self._disallowed_areas_no_brim
def setDisallowedAreas(self, areas: List[Polygon]): def setDisallowedAreas(self, areas: List[Polygon]):
self._disallowed_areas = areas self._disallowed_areas = areas
@ -457,7 +460,7 @@ class BuildVolume(SceneNode):
minimum = Vector(min_w, min_h - 1.0, min_d), minimum = Vector(min_w, min_h - 1.0, min_d),
maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d)) maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d))
bed_adhesion_size = self._getEdgeDisallowedSize() bed_adhesion_size = self.getEdgeDisallowedSize()
# As this works better for UM machines, we only add the disallowed_area_size for the z direction. # As this works better for UM machines, we only add the disallowed_area_size for the z direction.
# This is probably wrong in all other cases. TODO! # This is probably wrong in all other cases. TODO!
@ -649,7 +652,7 @@ class BuildVolume(SceneNode):
extruder_manager = ExtruderManager.getInstance() extruder_manager = ExtruderManager.getInstance()
used_extruders = extruder_manager.getUsedExtruderStacks() used_extruders = extruder_manager.getUsedExtruderStacks()
disallowed_border_size = self._getEdgeDisallowedSize() disallowed_border_size = self.getEdgeDisallowedSize()
if not used_extruders: if not used_extruders:
# If no extruder is used, assume that the active extruder is used (else nothing is drawn) # If no extruder is used, assume that the active extruder is used (else nothing is drawn)
@ -660,7 +663,8 @@ class BuildVolume(SceneNode):
result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) #Normal machine disallowed areas can always be added. result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) #Normal machine disallowed areas can always be added.
prime_areas = self._computeDisallowedAreasPrimeBlob(disallowed_border_size, used_extruders) prime_areas = self._computeDisallowedAreasPrimeBlob(disallowed_border_size, used_extruders)
prime_disallowed_areas = self._computeDisallowedAreasStatic(0, used_extruders) #Where the priming is not allowed to happen. This is not added to the result, just for collision checking. result_areas_no_brim = self._computeDisallowedAreasStatic(0, used_extruders) #Where the priming is not allowed to happen. This is not added to the result, just for collision checking.
prime_disallowed_areas = copy.deepcopy(result_areas_no_brim)
#Check if prime positions intersect with disallowed areas. #Check if prime positions intersect with disallowed areas.
for extruder in used_extruders: for extruder in used_extruders:
@ -689,12 +693,15 @@ class BuildVolume(SceneNode):
break break
result_areas[extruder_id].extend(prime_areas[extruder_id]) result_areas[extruder_id].extend(prime_areas[extruder_id])
result_areas_no_brim[extruder_id].extend(prime_areas[extruder_id])
nozzle_disallowed_areas = extruder.getProperty("nozzle_disallowed_areas", "value") nozzle_disallowed_areas = extruder.getProperty("nozzle_disallowed_areas", "value")
for area in nozzle_disallowed_areas: for area in nozzle_disallowed_areas:
polygon = Polygon(numpy.array(area, numpy.float32)) polygon = Polygon(numpy.array(area, numpy.float32))
polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size)) polygon_disallowed_border = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size))
result_areas[extruder_id].append(polygon) #Don't perform the offset on these. result_areas[extruder_id].append(polygon_disallowed_border) #Don't perform the offset on these.
#polygon_minimal_border = polygon.getMinkowskiHull(5)
result_areas_no_brim[extruder_id].append(polygon) # no brim
# Add prime tower location as disallowed area. # Add prime tower location as disallowed area.
if len(used_extruders) > 1: #No prime tower in single-extrusion. if len(used_extruders) > 1: #No prime tower in single-extrusion.
@ -710,6 +717,7 @@ class BuildVolume(SceneNode):
break break
if not prime_tower_collision: if not prime_tower_collision:
result_areas[extruder_id].extend(prime_tower_areas[extruder_id]) result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
else: else:
self._error_areas.extend(prime_tower_areas[extruder_id]) self._error_areas.extend(prime_tower_areas[extruder_id])
@ -718,6 +726,9 @@ class BuildVolume(SceneNode):
self._disallowed_areas = [] self._disallowed_areas = []
for extruder_id in result_areas: for extruder_id in result_areas:
self._disallowed_areas.extend(result_areas[extruder_id]) self._disallowed_areas.extend(result_areas[extruder_id])
self._disallowed_areas_no_brim = []
for extruder_id in result_areas_no_brim:
self._disallowed_areas_no_brim.extend(result_areas_no_brim[extruder_id])
## Computes the disallowed areas for objects that are printed with print ## Computes the disallowed areas for objects that are printed with print
# features. # features.
@ -951,12 +962,12 @@ class BuildVolume(SceneNode):
all_values[i] = 0 all_values[i] = 0
return all_values return all_values
## Convenience function to calculate the disallowed radius around the edge. ## Calculate the disallowed radius around the edge.
# #
# This disallowed radius is to allow for space around the models that is # This disallowed radius is to allow for space around the models that is
# not part of the collision radius, such as bed adhesion (skirt/brim/raft) # not part of the collision radius, such as bed adhesion (skirt/brim/raft)
# and travel avoid distance. # and travel avoid distance.
def _getEdgeDisallowedSize(self): def getEdgeDisallowedSize(self):
if not self._global_container_stack or not self._global_container_stack.extruders: if not self._global_container_stack or not self._global_container_stack.extruders:
return 0 return 0
@ -1037,6 +1048,6 @@ class BuildVolume(SceneNode):
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"] _prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"]
_tower_settings = ["prime_tower_enable", "prime_tower_circular", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"] _tower_settings = ["prime_tower_enable", "prime_tower_circular", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"]
_ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"] _ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
_distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts"] _distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports"]
_extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used. _extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
_limit_to_extruder_settings = ["wall_extruder_nr", "wall_0_extruder_nr", "wall_x_extruder_nr", "top_bottom_extruder_nr", "infill_extruder_nr", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "adhesion_extruder_nr"] _limit_to_extruder_settings = ["wall_extruder_nr", "wall_0_extruder_nr", "wall_x_extruder_nr", "top_bottom_extruder_nr", "infill_extruder_nr", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "adhesion_extruder_nr"]

View file

@ -72,7 +72,8 @@ class CuraActions(QObject):
# \param count The number of times to multiply the selection. # \param count The number of times to multiply the selection.
@pyqtSlot(int) @pyqtSlot(int)
def multiplySelection(self, count: int) -> None: def multiplySelection(self, count: int) -> None:
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = 8) min_offset = Application.getInstance().getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = max(min_offset, 8))
job.start() job.start()
## Delete all selected objects. ## Delete all selected objects.

View file

@ -85,6 +85,7 @@ from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
from cura.Machines.VariantManager import VariantManager from cura.Machines.VariantManager import VariantManager
from .SingleInstance import SingleInstance from .SingleInstance import SingleInstance
from .AutoSave import AutoSave
from . import PlatformPhysics from . import PlatformPhysics
from . import BuildVolume from . import BuildVolume
from . import CameraAnimation from . import CameraAnimation
@ -154,9 +155,6 @@ class CuraApplication(QtApplication):
self._boot_loading_time = time.time() self._boot_loading_time = time.time()
self._currently_loading_files = []
self._non_sliceable_extensions = []
# Variables set from CLI # Variables set from CLI
self._files_to_open = [] self._files_to_open = []
self._use_single_instance = False self._use_single_instance = False
@ -223,6 +221,10 @@ class CuraApplication(QtApplication):
self._need_to_show_user_agreement = True self._need_to_show_user_agreement = True
# Backups
self._auto_save = None
self._save_data_enabled = True
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
self._container_registry_class = CuraContainerRegistry self._container_registry_class = CuraContainerRegistry
@ -469,6 +471,7 @@ class CuraApplication(QtApplication):
preferences.addPreference("cura/categories_expanded", "") preferences.addPreference("cura/categories_expanded", "")
preferences.addPreference("cura/jobname_prefix", True) preferences.addPreference("cura/jobname_prefix", True)
preferences.addPreference("cura/select_models_on_load", False)
preferences.addPreference("view/center_on_select", False) preferences.addPreference("view/center_on_select", False)
preferences.addPreference("mesh/scale_to_fit", False) preferences.addPreference("mesh/scale_to_fit", False)
preferences.addPreference("mesh/scale_tiny_meshes", True) preferences.addPreference("mesh/scale_tiny_meshes", True)
@ -585,14 +588,17 @@ class CuraApplication(QtApplication):
showPrintMonitor = pyqtSignal(bool, arguments = ["show"]) showPrintMonitor = pyqtSignal(bool, arguments = ["show"])
## Cura has multiple locations where instance containers need to be saved, so we need to handle this differently. def setSaveDataEnabled(self, enabled: bool) -> None:
# self._save_data_enabled = enabled
# Note that the AutoSave plugin also calls this method.
def saveSettings(self):
if not self.started: # Do not do saving during application start
return
# Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
def saveSettings(self):
if not self.started or not self._save_data_enabled:
# Do not do saving during application start or when data should not be safed on quit.
return
ContainerRegistry.getInstance().saveDirtyContainers() ContainerRegistry.getInstance().saveDirtyContainers()
Preferences.getInstance().writeToFile(Resources.getStoragePath(Resources.Preferences,
self._application_name + ".cfg"))
def saveStack(self, stack): def saveStack(self, stack):
ContainerRegistry.getInstance().saveContainer(stack) ContainerRegistry.getInstance().saveContainer(stack)
@ -695,6 +701,9 @@ class CuraApplication(QtApplication):
self._post_start_timer.timeout.connect(self._onPostStart) self._post_start_timer.timeout.connect(self._onPostStart)
self._post_start_timer.start() self._post_start_timer.start()
self._auto_save = AutoSave(self)
self._auto_save.initialize()
self.exec_() self.exec_()
def __setUpSingleInstanceServer(self): def __setUpSingleInstanceServer(self):
@ -844,6 +853,9 @@ class CuraApplication(QtApplication):
return super().event(event) return super().event(event)
def getAutoSave(self):
return self._auto_save
## Get print information (duration / material used) ## Get print information (duration / material used)
def getPrintInformation(self): def getPrintInformation(self):
return self._print_information return self._print_information
@ -1228,34 +1240,12 @@ class CuraApplication(QtApplication):
nodes.append(node) nodes.append(node)
self.arrange(nodes, fixed_nodes = []) self.arrange(nodes, fixed_nodes = [])
## Arrange Selection
@pyqtSlot()
def arrangeSelection(self):
nodes = Selection.getAllSelectedObjects()
# What nodes are on the build plate and are not being moved
fixed_nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if not isinstance(node, SceneNode):
continue
if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group.
if node.getParent() and node.getParent().callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.isSelectable():
continue # i.e. node with layer data
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
continue # i.e. node with layer data
if node in nodes: # exclude selected node from fixed_nodes
continue
fixed_nodes.append(node)
self.arrange(nodes, fixed_nodes)
## Arrange a set of nodes given a set of fixed nodes ## Arrange a set of nodes given a set of fixed nodes
# \param nodes nodes that we have to place # \param nodes nodes that we have to place
# \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes # \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes
def arrange(self, nodes, fixed_nodes): def arrange(self, nodes, fixed_nodes):
job = ArrangeObjectsJob(nodes, fixed_nodes) min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8))
job.start() job.start()
## Reload all mesh data on the screen from file. ## Reload all mesh data on the screen from file.
@ -1539,6 +1529,9 @@ class CuraApplication(QtApplication):
self.callLater(self.openProjectFile.emit, file) self.callLater(self.openProjectFile.emit, file)
return return
if Preferences.getInstance().getValue("cura/select_models_on_load"):
Selection.clear()
f = file.toLocalFile() f = file.toLocalFile()
extension = os.path.splitext(f)[1] extension = os.path.splitext(f)[1]
filename = os.path.basename(f) filename = os.path.basename(f)
@ -1585,11 +1578,16 @@ class CuraApplication(QtApplication):
for node_ in DepthFirstIterator(root): for node_ in DepthFirstIterator(root):
if node_.callDecoration("isSliceable") and node_.callDecoration("getBuildPlateNumber") == target_build_plate: if node_.callDecoration("isSliceable") and node_.callDecoration("getBuildPlateNumber") == target_build_plate:
fixed_nodes.append(node_) fixed_nodes.append(node_)
arranger = Arrange.create(fixed_nodes = fixed_nodes) global_container_stack = self.getGlobalContainerStack()
machine_width = global_container_stack.getProperty("machine_width", "value")
machine_depth = global_container_stack.getProperty("machine_depth", "value")
arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = fixed_nodes)
min_offset = 8 min_offset = 8
default_extruder_position = self.getMachineManager().defaultExtruderPosition default_extruder_position = self.getMachineManager().defaultExtruderPosition
default_extruder_id = self._global_container_stack.extruders[default_extruder_position].getId() default_extruder_id = self._global_container_stack.extruders[default_extruder_position].getId()
select_models_on_load = Preferences.getInstance().getValue("cura/select_models_on_load")
for original_node in nodes: for original_node in nodes:
# Create a CuraSceneNode just if the original node is not that type # Create a CuraSceneNode just if the original node is not that type
@ -1603,7 +1601,6 @@ class CuraApplication(QtApplication):
if(original_node.getScale() != Vector(1.0, 1.0, 1.0)): if(original_node.getScale() != Vector(1.0, 1.0, 1.0)):
node.scale(original_node.getScale()) node.scale(original_node.getScale())
node.setSelectable(True) node.setSelectable(True)
node.setName(os.path.basename(filename)) node.setName(os.path.basename(filename))
self.getBuildVolume().checkBoundsAndUpdate(node) self.getBuildVolume().checkBoundsAndUpdate(node)
@ -1663,6 +1660,9 @@ class CuraApplication(QtApplication):
node.callDecoration("setActiveExtruder", default_extruder_id) node.callDecoration("setActiveExtruder", default_extruder_id)
scene.sceneChanged.emit(node) scene.sceneChanged.emit(node)
if select_models_on_load:
Selection.add(node)
self.fileCompleted.emit(filename) self.fileCompleted.emit(filename)
def addNonSliceableExtension(self, extension): def addNonSliceableExtension(self, extension):

View file

@ -30,11 +30,18 @@ class MultiplyObjectsJob(Job):
total_progress = len(self._objects) * self._count total_progress = len(self._objects) * self._count
current_progress = 0 current_progress = 0
global_container_stack = Application.getInstance().getGlobalContainerStack()
machine_width = global_container_stack.getProperty("machine_width", "value")
machine_depth = global_container_stack.getProperty("machine_depth", "value")
root = scene.getRoot() root = scene.getRoot()
arranger = Arrange.create(scene_root=root) scale = 0.5
arranger = Arrange.create(x = machine_width, y = machine_depth, scene_root = root, scale = scale)
processed_nodes = [] processed_nodes = []
nodes = [] nodes = []
not_fit_count = 0
for node in self._objects: for node in self._objects:
# If object is part of a group, multiply group # If object is part of a group, multiply group
current_node = node current_node = node
@ -46,12 +53,13 @@ class MultiplyObjectsJob(Job):
processed_nodes.append(current_node) processed_nodes.append(current_node)
node_too_big = False node_too_big = False
if node.getBoundingBox().width < 300 or node.getBoundingBox().depth < 300: if node.getBoundingBox().width < machine_width or node.getBoundingBox().depth < machine_depth:
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset=self._min_offset) offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset = self._min_offset, scale = scale)
else: else:
node_too_big = True node_too_big = True
found_solution_for_all = True found_solution_for_all = True
arranger.resetLastPriority()
for i in range(self._count): for i in range(self._count):
# We do place the nodes one by one, as we want to yield in between. # We do place the nodes one by one, as we want to yield in between.
if not node_too_big: if not node_too_big:
@ -59,8 +67,9 @@ class MultiplyObjectsJob(Job):
if node_too_big or not solution_found: if node_too_big or not solution_found:
found_solution_for_all = False found_solution_for_all = False
new_location = new_node.getPosition() new_location = new_node.getPosition()
new_location = new_location.set(z = 100 - i * 20) new_location = new_location.set(z = - not_fit_count * 20)
new_node.setPosition(new_location) new_node.setPosition(new_location)
not_fit_count += 1
# Same build plate # Same build plate
build_plate_number = current_node.callDecoration("getBuildPlateNumber") build_plate_number = current_node.callDecoration("getBuildPlateNumber")

View file

@ -79,10 +79,10 @@ class PreviewPass(RenderPass):
for node in DepthFirstIterator(self._scene.getRoot()): for node in DepthFirstIterator(self._scene.getRoot()):
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible(): if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
per_mesh_stack = node.callDecoration("getStack") per_mesh_stack = node.callDecoration("getStack")
if node.callDecoration("isNonPrintingMesh"): if node.callDecoration("isNonThumbnailVisibleMesh"):
# Non printing mesh # Non printing mesh
continue continue
elif per_mesh_stack is not None and per_mesh_stack.getProperty("support_mesh", "value") == True: elif per_mesh_stack is not None and per_mesh_stack.getProperty("support_mesh", "value"):
# Support mesh # Support mesh
uniforms = {} uniforms = {}
shade_factor = 0.6 shade_factor = 0.6
@ -112,4 +112,3 @@ class PreviewPass(RenderPass):
batch_support_mesh.render(render_camera) batch_support_mesh.render(render_camera)
self.release() self.release()

View file

@ -279,9 +279,12 @@ class PrintInformation(QObject):
for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1): for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
self._calculateInformation(build_plate_number) self._calculateInformation(build_plate_number)
# Manual override of job name should also set the base name so that when the printer prefix is updated, it the
# prefix can be added to the manually added name, not the old base name
@pyqtSlot(str) @pyqtSlot(str)
def setJobName(self, name): def setJobName(self, name):
self._job_name = name self._job_name = name
self._base_name = name.replace(self._abbr_machine + "_", "")
self.jobNameChanged.emit() self.jobNameChanged.emit()
jobNameChanged = pyqtSignal() jobNameChanged = pyqtSignal()

View file

@ -30,6 +30,7 @@ class SettingOverrideDecorator(SceneNodeDecorator):
# Note that Support Mesh is not in here because it actually generates # Note that Support Mesh is not in here because it actually generates
# g-code in the volume of the mesh. # g-code in the volume of the mesh.
_non_printing_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"} _non_printing_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"}
_non_thumbnail_visible_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh", "support_mesh"}
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -41,6 +42,7 @@ class SettingOverrideDecorator(SceneNodeDecorator):
self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0).getId() self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0).getId()
self._is_non_printing_mesh = False self._is_non_printing_mesh = False
self._is_non_thumbnail_visible_mesh = False
self._stack.propertyChanged.connect(self._onSettingChanged) self._stack.propertyChanged.connect(self._onSettingChanged)
@ -72,6 +74,7 @@ class SettingOverrideDecorator(SceneNodeDecorator):
# use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh" # use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh"
# has not been updated yet. # has not been updated yet.
deep_copy._is_non_printing_mesh = self.evaluateIsNonPrintingMesh() deep_copy._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
deep_copy._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh()
return deep_copy return deep_copy
@ -102,10 +105,17 @@ class SettingOverrideDecorator(SceneNodeDecorator):
def evaluateIsNonPrintingMesh(self): def evaluateIsNonPrintingMesh(self):
return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings) return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings)
def isNonThumbnailVisibleMesh(self):
return self._is_non_thumbnail_visible_mesh
def evaluateIsNonThumbnailVisibleMesh(self):
return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_thumbnail_visible_settings)
def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function
if property_name == "value": if property_name == "value":
# Trigger slice/need slicing if the value has changed. # Trigger slice/need slicing if the value has changed.
self._is_non_printing_mesh = self.evaluateIsNonPrintingMesh() self._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
self._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh()
Application.getInstance().getBackend().needsSlicing() Application.getInstance().getBackend().needsSlicing()
Application.getInstance().getBackend().tickle() Application.getInstance().getBackend().tickle()

View file

@ -48,7 +48,7 @@ class Snapshot:
# determine zoom and look at # determine zoom and look at
bbox = None bbox = None
for node in DepthFirstIterator(root): for node in DepthFirstIterator(root):
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration("isNonPrintingMesh"): if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration("isNonThumbnailVisibleMesh"):
if bbox is None: if bbox is None:
bbox = node.getBoundingBox() bbox = node.getBoundingBox()
else: else:

View file

@ -1,9 +1,10 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtProperty, QUrl, QObject from PyQt5.QtCore import pyqtProperty, QUrl
from UM.Stage import Stage from UM.Stage import Stage
class CuraStage(Stage): class CuraStage(Stage):
def __init__(self, parent = None): def __init__(self, parent = None):

View file

@ -1,76 +0,0 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QTimer
from UM.Extension import Extension
from UM.Application import Application
from UM.Resources import Resources
from UM.Logger import Logger
class AutoSave(Extension):
def __init__(self):
super().__init__()
Application.getInstance().getPreferences().preferenceChanged.connect(self._triggerTimer)
self._global_stack = None
Application.getInstance().getPreferences().addPreference("cura/autosave_delay", 1000 * 10)
self._change_timer = QTimer()
self._change_timer.setInterval(Application.getInstance().getPreferences().getValue("cura/autosave_delay"))
self._change_timer.setSingleShot(True)
self._saving = False
# At this point, the Application instance has not finished its constructor call yet, so directly using something
# like Application.getInstance() is not correct. The initialisation now will only gets triggered after the
# application finishes its start up successfully.
self._init_timer = QTimer()
self._init_timer.setInterval(1000)
self._init_timer.setSingleShot(True)
self._init_timer.timeout.connect(self.initialize)
self._init_timer.start()
def initialize(self):
# only initialise if the application is created and has started
from cura.CuraApplication import CuraApplication
if not CuraApplication.Created:
self._init_timer.start()
return
if not CuraApplication.getInstance().started:
self._init_timer.start()
return
self._change_timer.timeout.connect(self._onTimeout)
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
self._triggerTimer()
def _triggerTimer(self, *args):
if not self._saving:
self._change_timer.start()
def _onGlobalStackChanged(self):
if self._global_stack:
self._global_stack.propertyChanged.disconnect(self._triggerTimer)
self._global_stack.containersChanged.disconnect(self._triggerTimer)
self._global_stack = Application.getInstance().getGlobalContainerStack()
if self._global_stack:
self._global_stack.propertyChanged.connect(self._triggerTimer)
self._global_stack.containersChanged.connect(self._triggerTimer)
def _onTimeout(self):
self._saving = True # To prevent the save process from triggering another autosave.
Logger.log("d", "Autosaving preferences, instances and profiles")
Application.getInstance().saveSettings()
Application.getInstance().getPreferences().writeToFile(Resources.getStoragePath(Resources.Preferences, Application.getInstance().getApplicationName() + ".cfg"))
self._saving = False

View file

@ -1,13 +0,0 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import AutoSave
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {}
def register(app):
return { "extension": AutoSave.AutoSave() }

View file

@ -1,8 +0,0 @@
{
"name": "Auto Save",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Automatically saves Preferences, Machines and Profiles after changes.",
"api": 4,
"i18n-catalog": "cura"
}

View file

@ -9,7 +9,7 @@ from . import GCodeGzWriter
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
file_extension = "gz" if Platform.isOSX() else "gcode.gz" file_extension = "gcode.gz"
return { return {
"mesh_writer": { "mesh_writer": {
"output": [{ "output": [{

View file

@ -69,10 +69,11 @@ class MonitorStage(CuraStage):
self._updateSidebar() self._updateSidebar()
def _updateMainOverlay(self): def _updateMainOverlay(self):
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"), "MonitorMainView.qml") main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"),
"MonitorMainView.qml")
self.addDisplayComponent("main", main_component_path) self.addDisplayComponent("main", main_component_path)
def _updateSidebar(self): def _updateSidebar(self):
# TODO: currently the sidebar component for prepare and monitor stages is the same, this will change with the printer output device refactor! sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles),
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "Sidebar.qml") "MonitorSidebar.qml")
self.addDisplayComponent("sidebar", sidebar_component_path) self.addDisplayComponent("sidebar", sidebar_component_path)

View file

@ -117,12 +117,21 @@ class PauseAtHeight(Script):
} }
}""" }"""
def getNextXY(self, layer: str):
"""
Get the X and Y values for a layer (will be used to get X and Y of
the layer after the pause
"""
lines = layer.split("\n")
for line in lines:
if self.getValue(line, "X") is not None and self.getValue(line, "Y") is not None:
x = self.getValue(line, "X")
y = self.getValue(line, "Y")
return x, y
return 0, 0
def execute(self, data: list): def execute(self, data: list):
"""data is a list. Each index contains a layer""" """data is a list. Each index contains a layer"""
x = 0.
y = 0.
pause_at = self.getSettingValueByKey("pause_at") pause_at = self.getSettingValueByKey("pause_at")
pause_height = self.getSettingValueByKey("pause_height") pause_height = self.getSettingValueByKey("pause_height")
pause_layer = self.getSettingValueByKey("pause_layer") pause_layer = self.getSettingValueByKey("pause_layer")
@ -138,73 +147,94 @@ class PauseAtHeight(Script):
resume_temperature = self.getSettingValueByKey("resume_temperature") resume_temperature = self.getSettingValueByKey("resume_temperature")
# T = ExtruderManager.getInstance().getActiveExtruderStack().getProperty("material_print_temperature", "value") # T = ExtruderManager.getInstance().getActiveExtruderStack().getProperty("material_print_temperature", "value")
# with open("out.txt", "w") as f:
# f.write(T)
# use offset to calculate the current height: <current_height> = <current_z> - <layer_0_z> # use offset to calculate the current height: <current_height> = <current_z> - <layer_0_z>
layer_0_z = 0. layer_0_z = 0.
current_z = 0 current_z = 0
got_first_g_cmd_on_layer_0 = False got_first_g_cmd_on_layer_0 = False
nbr_negative_layers = 0
for index, layer in enumerate(data): for index, layer in enumerate(data):
lines = layer.split("\n") lines = layer.split("\n")
# Scroll each line of instruction for each layer in the G-code
for line in lines: for line in lines:
# Fist positive layer reached
if ";LAYER:0" in line: if ";LAYER:0" in line:
layers_started = True layers_started = True
# Count nbr of negative layers (raft)
elif ";LAYER:-" in line:
nbr_negative_layers += 1
if not layers_started: if not layers_started:
continue continue
# If a Z instruction is in the line, read the current Z
if self.getValue(line, "Z") is not None: if self.getValue(line, "Z") is not None:
current_z = self.getValue(line, "Z") current_z = self.getValue(line, "Z")
if pause_at == "height": if pause_at == "height":
# Ignore if the line is not G1 or G0
if self.getValue(line, "G") != 1 and self.getValue(line, "G") != 0: if self.getValue(line, "G") != 1 and self.getValue(line, "G") != 0:
continue continue
# This block is executed once, the first time there is a G
# command, to get the z offset (z for first positive layer)
if not got_first_g_cmd_on_layer_0: if not got_first_g_cmd_on_layer_0:
layer_0_z = current_z layer_0_z = current_z
got_first_g_cmd_on_layer_0 = True got_first_g_cmd_on_layer_0 = True
x = self.getValue(line, "X", x)
y = self.getValue(line, "Y", y)
current_height = current_z - layer_0_z current_height = current_z - layer_0_z
if current_height < pause_height: if current_height < pause_height:
break #Try the next layer. break # Try the next layer.
else: #Pause at layer.
# Pause at layer
else:
if not line.startswith(";LAYER:"): if not line.startswith(";LAYER:"):
continue continue
current_layer = line[len(";LAYER:"):] current_layer = line[len(";LAYER:"):]
try: try:
current_layer = int(current_layer) current_layer = int(current_layer)
except ValueError: #Couldn't cast to int. Something is wrong with this g-code data.
continue
if current_layer < pause_layer:
break #Try the next layer.
prevLayer = data[index - 1] # Couldn't cast to int. Something is wrong with this
prevLines = prevLayer.split("\n") # g-code data
except ValueError:
continue
if current_layer < pause_layer - nbr_negative_layers:
continue
# Get X and Y from the next layer (better position for
# the nozzle)
next_layer = data[index + 1]
x, y = self.getNextXY(next_layer)
prev_layer = data[index - 1]
prev_lines = prev_layer.split("\n")
current_e = 0. current_e = 0.
# Access last layer, browse it backwards to find # Access last layer, browse it backwards to find
# last extruder absolute position # last extruder absolute position
for prevLine in reversed(prevLines): for prevLine in reversed(prev_lines):
current_e = self.getValue(prevLine, "E", -1) current_e = self.getValue(prevLine, "E", -1)
if current_e >= 0: if current_e >= 0:
break break
# include a number of previous layers # include a number of previous layers
for i in range(1, redo_layers + 1): for i in range(1, redo_layers + 1):
prevLayer = data[index - i] prev_layer = data[index - i]
layer = prevLayer + layer layer = prev_layer + layer
# Get extruder's absolute position at the # Get extruder's absolute position at the
# begining of the first layer redone # beginning of the first layer redone
# see https://github.com/nallath/PostProcessingPlugin/issues/55 # see https://github.com/nallath/PostProcessingPlugin/issues/55
if i == redo_layers: if i == redo_layers:
prevLines = prevLayer.split("\n") # Get X and Y from the next layer (better position for
for line in prevLines: # the nozzle)
x, y = self.getNextXY(layer)
prev_lines = prev_layer.split("\n")
for line in prev_lines:
new_e = self.getValue(line, 'E', current_e) new_e = self.getValue(line, 'E', current_e)
if new_e != current_e: if new_e != current_e:
current_e = new_e current_e = new_e
break break
@ -213,61 +243,63 @@ class PauseAtHeight(Script):
prepend_gcode += ";added code by post processing\n" prepend_gcode += ";added code by post processing\n"
prepend_gcode += ";script: PauseAtHeight.py\n" prepend_gcode += ";script: PauseAtHeight.py\n"
if pause_at == "height": if pause_at == "height":
prepend_gcode += ";current z: {z}\n".format(z = current_z) prepend_gcode += ";current z: {z}\n".format(z=current_z)
prepend_gcode += ";current height: {height}\n".format(height = current_height) prepend_gcode += ";current height: {height}\n".format(height=current_height)
else: else:
prepend_gcode += ";current layer: {layer}\n".format(layer = current_layer) prepend_gcode += ";current layer: {layer}\n".format(layer=current_layer)
# Retraction # Retraction
prepend_gcode += self.putValue(M = 83) + "\n" prepend_gcode += self.putValue(M=83) + "\n"
if retraction_amount != 0: if retraction_amount != 0:
prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = retraction_speed * 60) + "\n" prepend_gcode += self.putValue(G=1, E=-retraction_amount, F=retraction_speed * 60) + "\n"
# Move the head away # Move the head away
prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + "\n" prepend_gcode += self.putValue(G=1, Z=current_z + 1, F=300) + "\n"
prepend_gcode += self.putValue(G = 1, X = park_x, Y = park_y, F = 9000) + "\n"
# This line should be ok
prepend_gcode += self.putValue(G=1, X=park_x, Y=park_y, F=9000) + "\n"
if current_z < 15: if current_z < 15:
prepend_gcode += self.putValue(G = 1, Z = 15, F = 300) + "\n" prepend_gcode += self.putValue(G=1, Z=15, F=300) + "\n"
# Disable the E steppers # Disable the E steppers
prepend_gcode += self.putValue(M = 84, E = 0) + "\n" prepend_gcode += self.putValue(M=84, E=0) + "\n"
# Set extruder standby temperature # Set extruder standby temperature
prepend_gcode += self.putValue(M = 104, S = standby_temperature) + "; standby temperature\n" prepend_gcode += self.putValue(M=104, S=standby_temperature) + "; standby temperature\n"
# Wait till the user continues printing # Wait till the user continues printing
prepend_gcode += self.putValue(M = 0) + ";Do the actual pause\n" prepend_gcode += self.putValue(M=0) + ";Do the actual pause\n"
# Set extruder resume temperature # Set extruder resume temperature
prepend_gcode += self.putValue(M = 109, S = resume_temperature) + "; resume temperature\n" prepend_gcode += self.putValue(M=109, S=resume_temperature) + "; resume temperature\n"
# Push the filament back, # Push the filament back,
if retraction_amount != 0: if retraction_amount != 0:
prepend_gcode += self.putValue(G = 1, E = retraction_amount, F = retraction_speed * 60) + "\n" prepend_gcode += self.putValue(G=1, E=retraction_amount, F=retraction_speed * 60) + "\n"
# Optionally extrude material # Optionally extrude material
if extrude_amount != 0: if extrude_amount != 0:
prepend_gcode += self.putValue(G = 1, E = extrude_amount, F = extrude_speed * 60) + "\n" prepend_gcode += self.putValue(G=1, E=extrude_amount, F=extrude_speed * 60) + "\n"
# and retract again, the properly primes the nozzle # and retract again, the properly primes the nozzle
# when changing filament. # when changing filament.
if retraction_amount != 0: if retraction_amount != 0:
prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = retraction_speed * 60) + "\n" prepend_gcode += self.putValue(G=1, E=-retraction_amount, F=retraction_speed * 60) + "\n"
# Move the head back # Move the head back
prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + "\n" prepend_gcode += self.putValue(G=1, Z=current_z + 1, F=300) + "\n"
prepend_gcode += self.putValue(G = 1, X = x, Y = y, F = 9000) + "\n" prepend_gcode += self.putValue(G=1, X=x, Y=y, F=9000) + "\n"
if retraction_amount != 0: if retraction_amount != 0:
prepend_gcode += self.putValue(G = 1, E = retraction_amount, F = retraction_speed * 60) + "\n" prepend_gcode += self.putValue(G=1, E=retraction_amount, F=retraction_speed * 60) + "\n"
prepend_gcode += self.putValue(G = 1, F = 9000) + "\n" prepend_gcode += self.putValue(G=1, F=9000) + "\n"
prepend_gcode += self.putValue(M = 82) + "\n" prepend_gcode += self.putValue(M=82) + "\n"
# reset extrude value to pre pause value # reset extrude value to pre pause value
prepend_gcode += self.putValue(G = 92, E = current_e) + "\n" prepend_gcode += self.putValue(G=92, E=current_e) + "\n"
layer = prepend_gcode + layer layer = prepend_gcode + layer
# Override the data of this layer with the # Override the data of this layer with the
# modified data # modified data
data[index] = layer data[index] = layer

View file

@ -14,5 +14,6 @@ class PrepareStage(CuraStage):
Application.getInstance().engineCreatedSignal.connect(self._engineCreated) Application.getInstance().engineCreatedSignal.connect(self._engineCreated)
def _engineCreated(self): def _engineCreated(self):
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "Sidebar.qml") sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles),
"PrepareSidebar.qml")
self.addDisplayComponent("sidebar", sidebar_component_path) self.addDisplayComponent("sidebar", sidebar_component_path)

View file

@ -24,17 +24,26 @@ from .PackagesModel import PackagesModel
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
## The Toolbox class is responsible of communicating with the server through the API ## The Toolbox class is responsible of communicating with the server through the API
class Toolbox(QObject, Extension): class Toolbox(QObject, Extension):
DEFAULT_PACKAGES_API_ROOT = "https://api.ultimaker.com"
def __init__(self, parent=None) -> None: def __init__(self, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self._application = Application.getInstance() self._application = Application.getInstance()
self._package_manager = None self._package_manager = None
self._plugin_registry = Application.getInstance().getPluginRegistry() self._plugin_registry = Application.getInstance().getPluginRegistry()
self._packages_api_root = self._getPackagesApiRoot()
self._packages_version = self._getPackagesVersion() self._packages_version = self._getPackagesVersion()
self._api_version = 1 self._api_version = 1
self._api_url = "https://api.ultimaker.com/cura-packages/v{api_version}/cura/v{package_version}".format( api_version = self._api_version, package_version = self._packages_version) self._api_url = "{api_root}/cura-packages/v{api_version}/cura/v{package_version}".format(
api_root = self._packages_api_root,
api_version = self._api_version,
package_version = self._packages_version
)
# Network: # Network:
self._get_packages_request = None self._get_packages_request = None
@ -153,6 +162,15 @@ class Toolbox(QObject, Extension):
def _onAppInitialized(self) -> None: def _onAppInitialized(self) -> None:
self._package_manager = Application.getInstance().getCuraPackageManager() self._package_manager = Application.getInstance().getCuraPackageManager()
# Get the API root for the packages API depending on Cura version settings.
def _getPackagesApiRoot(self) -> str:
if not hasattr(cura, "CuraVersion"):
return self.DEFAULT_PACKAGES_API_ROOT
if not hasattr(cura.CuraVersion, "CuraPackagesApiRoot"):
return self.DEFAULT_PACKAGES_API_ROOT
return cura.CuraVersion.CuraPackagesApiRoot
# Get the packages version depending on Cura version settings.
def _getPackagesVersion(self) -> int: def _getPackagesVersion(self) -> int:
if not hasattr(cura, "CuraVersion"): if not hasattr(cura, "CuraVersion"):
return self._plugin_registry.APIVersion return self._plugin_registry.APIVersion

View file

@ -308,7 +308,21 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
if b"ok T:" in line or line.startswith(b"T:") or b"ok B:" in line or line.startswith(b"B:"): # Temperature message. 'T:' for extruder and 'B:' for bed if b"ok T:" in line or line.startswith(b"T:") or b"ok B:" in line or line.startswith(b"B:"): # Temperature message. 'T:' for extruder and 'B:' for bed
extruder_temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line) extruder_temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line)
# Update all temperature values # Update all temperature values
for match, extruder in zip(extruder_temperature_matches, self._printers[0].extruders): matched_extruder_nrs = []
for match in extruder_temperature_matches:
extruder_nr = 0
if match[0] != b"":
extruder_nr = int(match[0])
if extruder_nr in matched_extruder_nrs:
continue
matched_extruder_nrs.append(extruder_nr)
if extruder_nr >= len(self._printers[0].extruders):
Logger.log("w", "Printer reports more temperatures than the number of configured extruders")
continue
extruder = self._printers[0].extruders[extruder_nr]
if match[1]: if match[1]:
extruder.updateHotendTemperature(float(match[1])) extruder.updateHotendTemperature(float(match[1]))
if match[2]: if match[2]:

View file

@ -60,7 +60,7 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
self._check_updates = True self._check_updates = True
self._update_thread.start() self._update_thread.start()
def stop(self): def stop(self, store_data: bool = True):
self._check_updates = False self._check_updates = False
def _onConnectionStateChanged(self, serial_port): def _onConnectionStateChanged(self, serial_port):
@ -79,10 +79,11 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
if container_stack is None: if container_stack is None:
time.sleep(5) time.sleep(5)
continue continue
port_list = [] # Just an empty list; all USB devices will be removed.
if container_stack.getMetaDataEntry("supports_usb_connection"): if container_stack.getMetaDataEntry("supports_usb_connection"):
port_list = self.getSerialPortList(only_list_usb=True) machine_file_formats = [file_type.strip() for file_type in container_stack.getMetaDataEntry("file_formats").split(";")]
else: if "text/x-gcode" in machine_file_formats:
port_list = [] # Just use an empty list; all USB devices will be removed. port_list = self.getSerialPortList(only_list_usb=True)
self._addRemovePorts(port_list) self._addRemovePorts(port_list)
time.sleep(5) time.sleep(5)

View file

@ -33,23 +33,6 @@
} }
} }
}, },
"AutoSave": {
"package_info": {
"package_id": "AutoSave",
"package_type": "plugin",
"display_name": "Auto-Save",
"description": "Automatically saves Preferences, Machines and Profiles after changes.",
"package_version": "1.0.0",
"cura_version": 4,
"website": "https://ultimaker.com",
"author": {
"author_id": "Ultimaker",
"display_name": "Ultimaker B.V.",
"email": "plugins@ultimaker.com",
"website": "https://ultimaker.com"
}
}
},
"ChangeLogPlugin": { "ChangeLogPlugin": {
"package_info": { "package_info": {
"package_id": "ChangeLogPlugin", "package_id": "ChangeLogPlugin",

View file

@ -3324,6 +3324,16 @@
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": true "settable_per_extruder": true
}, },
"travel_avoid_supports":
{
"label": "Avoid Supports When Traveling",
"description": "The nozzle avoids already printed supports when traveling. This option is only available when combing is enabled.",
"type": "bool",
"default_value": false,
"enabled": "resolveOrValue('retraction_combing') != 'off' and travel_avoid_other_parts",
"settable_per_mesh": false,
"settable_per_extruder": true
},
"travel_avoid_distance": "travel_avoid_distance":
{ {
"label": "Travel Avoid Distance", "label": "Travel Avoid Distance",

View file

@ -3106,6 +3106,18 @@ msgid ""
"available when combing is enabled." "available when combing is enabled."
msgstr "" msgstr ""
#: fdmprinter.def.json
msgctxt "travel_avoid_supports label"
msgid "Avoid Supports When Traveling"
msgstr ""
#: fdmprinter.def.json
msgctxt "travel_avoid_supports description"
msgid ""
"The nozzle avoids already printed supports when traveling. This option is only "
"available when combing is enabled."
msgstr ""
#: fdmprinter.def.json #: fdmprinter.def.json
msgctxt "travel_avoid_distance label" msgctxt "travel_avoid_distance label"
msgid "Travel Avoid Distance" msgid "Travel Avoid Distance"

View file

@ -15,6 +15,8 @@ Item
id: base; id: base;
UM.I18nCatalog { id: catalog; name:"cura"} UM.I18nCatalog { id: catalog; name:"cura"}
height: childrenRect.height + UM.Theme.getSize("sidebar_margin").height
property bool printerConnected: Cura.MachineManager.printerConnected property bool printerConnected: Cura.MachineManager.printerConnected
property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands
property var activePrinter: printerConnected ? Cura.MachineManager.printerOutputDevices[0].activePrinter : null property var activePrinter: printerConnected ? Cura.MachineManager.printerOutputDevices[0].activePrinter : null

View file

@ -0,0 +1,211 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.3
import UM 1.2 as UM
import Cura 1.0 as Cura
import "Menus"
import "Menus/ConfigurationMenu"
Rectangle
{
id: base
property int currentModeIndex
property bool hideSettings: PrintInformation.preSliced
property bool hideView: Cura.MachineManager.activeMachineName == ""
// Is there an output device for this printer?
property bool isNetworkPrinter: Cura.MachineManager.activeMachineNetworkKey != ""
property bool printerConnected: Cura.MachineManager.printerConnected
property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands
property var connectedPrinter: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null
property variant printDuration: PrintInformation.currentPrintTime
property variant printMaterialLengths: PrintInformation.materialLengths
property variant printMaterialWeights: PrintInformation.materialWeights
property variant printMaterialCosts: PrintInformation.materialCosts
property variant printMaterialNames: PrintInformation.materialNames
color: UM.Theme.getColor("sidebar")
UM.I18nCatalog { id: catalog; name:"cura"}
Timer {
id: tooltipDelayTimer
interval: 500
repeat: false
property var item
property string text
onTriggered:
{
base.showTooltip(base, {x: 0, y: item.y}, text);
}
}
function showTooltip(item, position, text)
{
tooltip.text = text;
position = item.mapToItem(base, position.x - UM.Theme.getSize("default_arrow").width, position.y);
tooltip.show(position);
}
function hideTooltip()
{
tooltip.hide();
}
function strPadLeft(string, pad, length) {
return (new Array(length + 1).join(pad) + string).slice(-length);
}
function getPrettyTime(time)
{
var hours = Math.floor(time / 3600)
time -= hours * 3600
var minutes = Math.floor(time / 60);
time -= minutes * 60
var seconds = Math.floor(time);
var finalTime = strPadLeft(hours, "0", 2) + ':' + strPadLeft(minutes,'0',2)+ ':' + strPadLeft(seconds,'0',2);
return finalTime;
}
MouseArea
{
anchors.fill: parent
acceptedButtons: Qt.AllButtons
onWheel:
{
wheel.accepted = true;
}
}
MachineSelection
{
id: machineSelection
width: base.width - configSelection.width - separator.width
height: UM.Theme.getSize("sidebar_header").height
anchors.top: base.top
anchors.left: parent.left
}
Rectangle
{
id: separator
visible: configSelection.visible
width: visible ? Math.round(UM.Theme.getSize("sidebar_lining_thin").height / 2) : 0
height: UM.Theme.getSize("sidebar_header").height
color: UM.Theme.getColor("sidebar_lining_thin")
anchors.left: machineSelection.right
}
ConfigurationSelection
{
id: configSelection
visible: isNetworkPrinter && printerConnected
width: visible ? Math.round(base.width * 0.15) : 0
height: UM.Theme.getSize("sidebar_header").height
anchors.top: base.top
anchors.right: parent.right
panelWidth: base.width
}
Loader
{
id: controlItem
anchors.bottom: footerSeparator.top
anchors.top: machineSelection.bottom
anchors.left: base.left
anchors.right: base.right
sourceComponent:
{
if(connectedPrinter != null)
{
if(connectedPrinter.controlItem != null)
{
return connectedPrinter.controlItem
}
}
return null
}
}
Loader
{
anchors.bottom: footerSeparator.top
anchors.top: machineSelection.bottom
anchors.left: base.left
anchors.right: base.right
source:
{
if(controlItem.sourceComponent == null)
{
return "PrintMonitor.qml"
}
else
{
return ""
}
}
}
Rectangle
{
id: footerSeparator
width: parent.width
height: UM.Theme.getSize("sidebar_lining").height
color: UM.Theme.getColor("sidebar_lining")
anchors.bottom: monitorButton.top
anchors.bottomMargin: UM.Theme.getSize("sidebar_margin").height
}
// MonitorButton is actually the bottom footer panel.
MonitorButton
{
id: monitorButton
implicitWidth: base.width
anchors.bottom: parent.bottom
}
SidebarTooltip
{
id: tooltip
}
UM.SettingPropertyProvider
{
id: machineExtruderCount
containerStackId: Cura.MachineManager.activeMachineId
key: "machine_extruder_count"
watchedProperties: [ "value" ]
storeIndex: 0
}
UM.SettingPropertyProvider
{
id: machineHeatedBed
containerStackId: Cura.MachineManager.activeMachineId
key: "machine_heated_bed"
watchedProperties: [ "value" ]
storeIndex: 0
}
// Make the ConfigurationSelector react when the global container changes, otherwise if Cura is not connected to the printer,
// switching printers make no reaction
Connections
{
target: Cura.MachineManager
onGlobalContainerChanged:
{
base.isNetworkPrinter = Cura.MachineManager.activeMachineNetworkKey != ""
base.printerConnected = Cura.MachineManager.printerOutputDevices.length != 0
}
}
}

View file

@ -79,6 +79,8 @@ UM.PreferencesPage
scaleToFitCheckbox.checked = boolCheck(UM.Preferences.getValue("mesh/scale_to_fit")) scaleToFitCheckbox.checked = boolCheck(UM.Preferences.getValue("mesh/scale_to_fit"))
UM.Preferences.resetPreference("mesh/scale_tiny_meshes") UM.Preferences.resetPreference("mesh/scale_tiny_meshes")
scaleTinyCheckbox.checked = boolCheck(UM.Preferences.getValue("mesh/scale_tiny_meshes")) scaleTinyCheckbox.checked = boolCheck(UM.Preferences.getValue("mesh/scale_tiny_meshes"))
UM.Preferences.resetPreference("cura/select_models_on_load")
selectModelsOnLoadCheckbox.checked = boolCheck(UM.Preferences.getValue("cura/select_models_on_load"))
UM.Preferences.resetPreference("cura/jobname_prefix") UM.Preferences.resetPreference("cura/jobname_prefix")
prefixJobNameCheckbox.checked = boolCheck(UM.Preferences.getValue("cura/jobname_prefix")) prefixJobNameCheckbox.checked = boolCheck(UM.Preferences.getValue("cura/jobname_prefix"))
UM.Preferences.resetPreference("view/show_overhang"); UM.Preferences.resetPreference("view/show_overhang");
@ -498,6 +500,21 @@ UM.PreferencesPage
} }
} }
UM.TooltipArea
{
width: childrenRect.width
height: childrenRect.height
text: catalog.i18nc("@info:tooltip","Should models be selected after they are loaded?")
CheckBox
{
id: selectModelsOnLoadCheckbox
text: catalog.i18nc("@option:check","Select models when loaded")
checked: boolCheck(UM.Preferences.getValue("cura/select_models_on_load"))
onCheckedChanged: UM.Preferences.setValue("cura/select_models_on_load", checked)
}
}
UM.TooltipArea UM.TooltipArea
{ {
width: childrenRect.width width: childrenRect.width

View file

@ -24,8 +24,6 @@ Rectangle
property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands
property var connectedPrinter: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null property var connectedPrinter: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null
property bool monitoringPrint: UM.Controller.activeStage.stageId == "MonitorStage"
property variant printDuration: PrintInformation.currentPrintTime property variant printDuration: PrintInformation.currentPrintTime
property variant printMaterialLengths: PrintInformation.materialLengths property variant printMaterialLengths: PrintInformation.materialLengths
property variant printMaterialWeights: PrintInformation.materialWeights property variant printMaterialWeights: PrintInformation.materialWeights
@ -120,7 +118,7 @@ Rectangle
SidebarHeader { SidebarHeader {
id: header id: header
width: parent.width width: parent.width
visible: !hideSettings && (machineExtruderCount.properties.value > 1 || Cura.MachineManager.hasMaterials || Cura.MachineManager.hasVariants) && !monitoringPrint visible: !hideSettings && (machineExtruderCount.properties.value > 1 || Cura.MachineManager.hasMaterials || Cura.MachineManager.hasVariants)
anchors.top: machineSelection.bottom anchors.top: machineSelection.bottom
onShowTooltip: base.showTooltip(item, location, text) onShowTooltip: base.showTooltip(item, location, text)
@ -158,7 +156,7 @@ Rectangle
width: Math.round(parent.width * 0.45) width: Math.round(parent.width * 0.45)
font: UM.Theme.getFont("large") font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")
visible: !monitoringPrint && !hideView visible: !hideView
} }
// Settings mode selection toggle // Settings mode selection toggle
@ -185,7 +183,7 @@ Rectangle
} }
} }
visible: !monitoringPrint && !hideSettings && !hideView visible: !hideSettings && !hideView
Component Component
{ {
@ -282,7 +280,7 @@ Rectangle
anchors.topMargin: UM.Theme.getSize("sidebar_margin").height anchors.topMargin: UM.Theme.getSize("sidebar_margin").height
anchors.left: base.left anchors.left: base.left
anchors.right: base.right anchors.right: base.right
visible: !monitoringPrint && !hideSettings visible: !hideSettings
replaceEnter: Transition { replaceEnter: Transition {
PropertyAnimation { PropertyAnimation {
@ -305,47 +303,11 @@ Rectangle
Loader Loader
{ {
id: controlItem
anchors.bottom: footerSeparator.top anchors.bottom: footerSeparator.top
anchors.top: monitoringPrint ? machineSelection.bottom : headerSeparator.bottom anchors.top: headerSeparator.bottom
anchors.left: base.left anchors.left: base.left
anchors.right: base.right anchors.right: base.right
sourceComponent: source: "SidebarContents.qml"
{
if(monitoringPrint && connectedPrinter != null)
{
if(connectedPrinter.controlItem != null)
{
return connectedPrinter.controlItem
}
}
return null
}
}
Loader
{
anchors.bottom: footerSeparator.top
anchors.top: monitoringPrint ? machineSelection.bottom : headerSeparator.bottom
anchors.left: base.left
anchors.right: base.right
source:
{
if(controlItem.sourceComponent == null)
{
if(monitoringPrint)
{
return "PrintMonitor.qml"
} else
{
return "SidebarContents.qml"
}
}
else
{
return ""
}
}
} }
Rectangle Rectangle
@ -367,7 +329,6 @@ Rectangle
anchors.bottomMargin: UM.Theme.getSize("sidebar_margin").height anchors.bottomMargin: UM.Theme.getSize("sidebar_margin").height
height: timeDetails.height + costSpec.height height: timeDetails.height + costSpec.height
width: base.width - (saveButton.buttonRowWidth + UM.Theme.getSize("sidebar_margin").width) width: base.width - (saveButton.buttonRowWidth + UM.Theme.getSize("sidebar_margin").width)
visible: !monitoringPrint
clip: true clip: true
Label Label
@ -570,8 +531,7 @@ Rectangle
} }
} }
// SaveButton and MonitorButton are actually the bottom footer panels. // SaveButton is actually the bottom footer panel.
// "!monitoringPrint" currently means "show-settings-mode"
SaveButton SaveButton
{ {
id: saveButton id: saveButton
@ -579,17 +539,6 @@ Rectangle
anchors.top: footerSeparator.bottom anchors.top: footerSeparator.bottom
anchors.topMargin: UM.Theme.getSize("sidebar_margin").height anchors.topMargin: UM.Theme.getSize("sidebar_margin").height
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
visible: !monitoringPrint
}
MonitorButton
{
id: monitorButton
implicitWidth: base.width
anchors.top: footerSeparator.bottom
anchors.topMargin: UM.Theme.getSize("sidebar_margin").height
anchors.bottom: parent.bottom
visible: monitoringPrint
} }
SidebarTooltip SidebarTooltip

View file

@ -17,7 +17,17 @@ Column
property int currentExtruderIndex: Cura.ExtruderManager.activeExtruderIndex; property int currentExtruderIndex: Cura.ExtruderManager.activeExtruderIndex;
property bool currentExtruderVisible: extrudersList.visible; property bool currentExtruderVisible: extrudersList.visible;
property bool printerConnected: Cura.MachineManager.printerConnected property bool printerConnected: Cura.MachineManager.printerConnected
property bool hasManyPrinterTypes: printerConnected ? Cura.MachineManager.printerOutputDevices[0].connectedPrintersTypeCount.length > 1 : false property bool hasManyPrinterTypes:
{
if (printerConnected)
{
if (Cura.MachineManager.printerOutputDevices[0].connectedPrintersTypeCount != null)
{
return Cura.MachineManager.printerOutputDevices[0].connectedPrintersTypeCount.length > 1;
}
}
return false;
}
property bool buildplateCompatibilityError: !Cura.MachineManager.variantBuildplateCompatible && !Cura.MachineManager.variantBuildplateUsable property bool buildplateCompatibilityError: !Cura.MachineManager.variantBuildplateCompatible && !Cura.MachineManager.variantBuildplateUsable
property bool buildplateCompatibilityWarning: Cura.MachineManager.variantBuildplateUsable property bool buildplateCompatibilityWarning: Cura.MachineManager.variantBuildplateUsable

View file

@ -72,6 +72,7 @@ jerk_enabled
[travel] [travel]
retraction_combing retraction_combing
travel_avoid_other_parts travel_avoid_other_parts
travel_avoid_supports
travel_avoid_distance travel_avoid_distance
retraction_hop_enabled retraction_hop_enabled
retraction_hop_only_when_collides retraction_hop_only_when_collides

View file

@ -187,6 +187,7 @@ jerk_skirt_brim
retraction_combing retraction_combing
travel_retract_before_outer_wall travel_retract_before_outer_wall
travel_avoid_other_parts travel_avoid_other_parts
travel_avoid_supports
travel_avoid_distance travel_avoid_distance
start_layers_at_same_position start_layers_at_same_position
layer_start_x layer_start_x

View file

@ -4,9 +4,27 @@ from cura.Arranging.Arrange import Arrange
from cura.Arranging.ShapeArray import ShapeArray from cura.Arranging.ShapeArray import ShapeArray
def gimmeShapeArray(): ## Triangle of area 12
vertices = numpy.array([[-3, 1], [3, 1], [0, -3]]) def gimmeTriangle():
shape_arr = ShapeArray.fromPolygon(vertices) return numpy.array([[-3, 1], [3, 1], [0, -3]], dtype=numpy.int32)
## Boring square
def gimmeSquare():
return numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32)
## Triangle of area 12
def gimmeShapeArray(scale = 1.0):
vertices = gimmeTriangle()
shape_arr = ShapeArray.fromPolygon(vertices, scale = scale)
return shape_arr
## Boring square
def gimmeShapeArraySquare(scale = 1.0):
vertices = gimmeSquare()
shape_arr = ShapeArray.fromPolygon(vertices, scale = scale)
return shape_arr return shape_arr
@ -20,9 +38,48 @@ def test_smoke_ShapeArray():
shape_arr = gimmeShapeArray() shape_arr = gimmeShapeArray()
## Test ShapeArray
def test_ShapeArray():
scale = 1
ar = Arrange(16, 16, 8, 8, scale = scale)
ar.centerFirst()
shape_arr = gimmeShapeArray(scale)
print(shape_arr.arr)
count = len(numpy.where(shape_arr.arr == 1)[0])
print(count)
assert count >= 10 # should approach 12
## Test ShapeArray with scaling
def test_ShapeArray_scaling():
scale = 2
ar = Arrange(16, 16, 8, 8, scale = scale)
ar.centerFirst()
shape_arr = gimmeShapeArray(scale)
print(shape_arr.arr)
count = len(numpy.where(shape_arr.arr == 1)[0])
print(count)
assert count >= 40 # should approach 2*2*12 = 48
## Test ShapeArray with scaling
def test_ShapeArray_scaling2():
scale = 0.5
ar = Arrange(16, 16, 8, 8, scale = scale)
ar.centerFirst()
shape_arr = gimmeShapeArray(scale)
print(shape_arr.arr)
count = len(numpy.where(shape_arr.arr == 1)[0])
print(count)
assert count >= 1 # should approach 3, but it can be inaccurate due to pixel rounding
## Test centerFirst ## Test centerFirst
def test_centerFirst(): def test_centerFirst():
ar = Arrange(300, 300, 150, 150) ar = Arrange(300, 300, 150, 150, scale = 1)
ar.centerFirst() ar.centerFirst()
assert ar._priority[150][150] < ar._priority[170][150] assert ar._priority[150][150] < ar._priority[170][150]
assert ar._priority[150][150] < ar._priority[150][170] assert ar._priority[150][150] < ar._priority[150][170]
@ -32,19 +89,39 @@ def test_centerFirst():
assert ar._priority[150][150] < ar._priority[130][130] assert ar._priority[150][150] < ar._priority[130][130]
## Test centerFirst
def test_centerFirst_rectangular():
ar = Arrange(400, 300, 200, 150, scale = 1)
ar.centerFirst()
assert ar._priority[150][200] < ar._priority[150][220]
assert ar._priority[150][200] < ar._priority[170][200]
assert ar._priority[150][200] < ar._priority[170][220]
assert ar._priority[150][200] < ar._priority[180][150]
assert ar._priority[150][200] < ar._priority[130][200]
assert ar._priority[150][200] < ar._priority[130][180]
## Test centerFirst
def test_centerFirst_rectangular():
ar = Arrange(10, 20, 5, 10, scale = 1)
ar.centerFirst()
print(ar._priority)
assert ar._priority[10][5] < ar._priority[10][7]
## Test backFirst ## Test backFirst
def test_backFirst(): def test_backFirst():
ar = Arrange(300, 300, 150, 150) ar = Arrange(300, 300, 150, 150, scale = 1)
ar.backFirst() ar.backFirst()
assert ar._priority[150][150] < ar._priority[150][170] assert ar._priority[150][150] < ar._priority[170][150]
assert ar._priority[150][150] < ar._priority[170][170] assert ar._priority[150][150] < ar._priority[170][170]
assert ar._priority[150][150] > ar._priority[150][130] assert ar._priority[150][150] > ar._priority[130][150]
assert ar._priority[150][150] > ar._priority[130][130] assert ar._priority[150][150] > ar._priority[130][130]
## See if the result of bestSpot has the correct form ## See if the result of bestSpot has the correct form
def test_smoke_bestSpot(): def test_smoke_bestSpot():
ar = Arrange(30, 30, 15, 15) ar = Arrange(30, 30, 15, 15, scale = 1)
ar.centerFirst() ar.centerFirst()
shape_arr = gimmeShapeArray() shape_arr = gimmeShapeArray()
@ -55,6 +132,113 @@ def test_smoke_bestSpot():
assert hasattr(best_spot, "priority") assert hasattr(best_spot, "priority")
## Real life test
def test_bestSpot():
ar = Arrange(16, 16, 8, 8, scale = 1)
ar.centerFirst()
shape_arr = gimmeShapeArray()
best_spot = ar.bestSpot(shape_arr)
assert best_spot.x == 0
assert best_spot.y == 0
ar.place(best_spot.x, best_spot.y, shape_arr)
# Place object a second time
best_spot = ar.bestSpot(shape_arr)
assert best_spot.x is not None # we found a location
assert best_spot.x != 0 or best_spot.y != 0 # it can't be on the same location
ar.place(best_spot.x, best_spot.y, shape_arr)
print(ar._occupied) # For debugging
## Real life test rectangular build plate
def test_bestSpot_rectangular_build_plate():
ar = Arrange(16, 40, 8, 20, scale = 1)
ar.centerFirst()
shape_arr = gimmeShapeArray()
best_spot = ar.bestSpot(shape_arr)
ar.place(best_spot.x, best_spot.y, shape_arr)
assert best_spot.x == 0
assert best_spot.y == 0
# Place object a second time
best_spot2 = ar.bestSpot(shape_arr)
assert best_spot2.x is not None # we found a location
assert best_spot2.x != 0 or best_spot2.y != 0 # it can't be on the same location
ar.place(best_spot2.x, best_spot2.y, shape_arr)
# Place object a 3rd time
best_spot3 = ar.bestSpot(shape_arr)
assert best_spot3.x is not None # we found a location
assert best_spot3.x != best_spot.x or best_spot3.y != best_spot.y # it can't be on the same location
assert best_spot3.x != best_spot2.x or best_spot3.y != best_spot2.y # it can't be on the same location
ar.place(best_spot3.x, best_spot3.y, shape_arr)
best_spot_x = ar.bestSpot(shape_arr)
ar.place(best_spot_x.x, best_spot_x.y, shape_arr)
best_spot_x = ar.bestSpot(shape_arr)
ar.place(best_spot_x.x, best_spot_x.y, shape_arr)
best_spot_x = ar.bestSpot(shape_arr)
ar.place(best_spot_x.x, best_spot_x.y, shape_arr)
print(ar._occupied) # For debugging
## Real life test
def test_bestSpot_scale():
scale = 0.5
ar = Arrange(16, 16, 8, 8, scale = scale)
ar.centerFirst()
shape_arr = gimmeShapeArray(scale)
best_spot = ar.bestSpot(shape_arr)
assert best_spot.x == 0
assert best_spot.y == 0
ar.place(best_spot.x, best_spot.y, shape_arr)
print(ar._occupied)
# Place object a second time
best_spot = ar.bestSpot(shape_arr)
assert best_spot.x is not None # we found a location
assert best_spot.x != 0 or best_spot.y != 0 # it can't be on the same location
ar.place(best_spot.x, best_spot.y, shape_arr)
print(ar._occupied) # For debugging
## Real life test
def test_bestSpot_scale_rectangular():
scale = 0.5
ar = Arrange(16, 40, 8, 20, scale = scale)
ar.centerFirst()
shape_arr = gimmeShapeArray(scale)
shape_arr_square = gimmeShapeArraySquare(scale)
best_spot = ar.bestSpot(shape_arr_square)
assert best_spot.x == 0
assert best_spot.y == 0
ar.place(best_spot.x, best_spot.y, shape_arr_square)
print(ar._occupied)
# Place object a second time
best_spot = ar.bestSpot(shape_arr)
assert best_spot.x is not None # we found a location
assert best_spot.x != 0 or best_spot.y != 0 # it can't be on the same location
ar.place(best_spot.x, best_spot.y, shape_arr)
best_spot = ar.bestSpot(shape_arr_square)
ar.place(best_spot.x, best_spot.y, shape_arr_square)
print(ar._occupied) # For debugging
## Try to place an object and see if something explodes ## Try to place an object and see if something explodes
def test_smoke_place(): def test_smoke_place():
ar = Arrange(30, 30, 15, 15) ar = Arrange(30, 30, 15, 15)
@ -80,6 +264,20 @@ def test_checkShape():
assert points3 > points assert points3 > points
## See of our center has less penalty points than out of the center
def test_checkShape_rectangular():
ar = Arrange(20, 30, 10, 15)
ar.centerFirst()
print(ar._priority)
shape_arr = gimmeShapeArray()
points = ar.checkShape(0, 0, shape_arr)
points2 = ar.checkShape(5, 0, shape_arr)
points3 = ar.checkShape(0, 5, shape_arr)
assert points2 > points
assert points3 > points
## Check that placing an object on occupied place returns None. ## Check that placing an object on occupied place returns None.
def test_checkShape_place(): def test_checkShape_place():
ar = Arrange(30, 30, 15, 15) ar = Arrange(30, 30, 15, 15)
@ -95,7 +293,7 @@ def test_checkShape_place():
## Test the whole sequence ## Test the whole sequence
def test_smoke_place_objects(): def test_smoke_place_objects():
ar = Arrange(20, 20, 10, 10) ar = Arrange(20, 20, 10, 10, scale = 1)
ar.centerFirst() ar.centerFirst()
shape_arr = gimmeShapeArray() shape_arr = gimmeShapeArray()
@ -104,6 +302,13 @@ def test_smoke_place_objects():
ar.place(best_spot_x, best_spot_y, shape_arr) ar.place(best_spot_x, best_spot_y, shape_arr)
# Test some internals
def test_compare_occupied_and_priority_tables():
ar = Arrange(10, 15, 5, 7)
ar.centerFirst()
assert ar._priority.shape == ar._occupied.shape
## Polygon -> array ## Polygon -> array
def test_arrayFromPolygon(): def test_arrayFromPolygon():
vertices = numpy.array([[-3, 1], [3, 1], [0, -3]]) vertices = numpy.array([[-3, 1], [3, 1], [0, -3]])
@ -145,3 +350,30 @@ def test_check2():
assert numpy.any(check_array) assert numpy.any(check_array)
assert not check_array[3][0] assert not check_array[3][0]
assert check_array[3][4] assert check_array[3][4]
## Just adding some stuff to ensure fromNode works as expected. Some parts should actually be in UM
def test_parts_of_fromNode():
from UM.Math.Polygon import Polygon
p = Polygon(numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32))
offset = 1
print(p._points)
p_offset = p.getMinkowskiHull(Polygon.approximatedCircle(offset))
print("--------------")
print(p_offset._points)
assert len(numpy.where(p_offset._points[:, 0] >= 2.9)) > 0
assert len(numpy.where(p_offset._points[:, 0] <= -2.9)) > 0
assert len(numpy.where(p_offset._points[:, 1] >= 2.9)) > 0
assert len(numpy.where(p_offset._points[:, 1] <= -2.9)) > 0
def test_parts_of_fromNode2():
from UM.Math.Polygon import Polygon
p = Polygon(numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32) * 2) # 4x4
offset = 13.3
scale = 0.5
p_offset = p.getMinkowskiHull(Polygon.approximatedCircle(offset))
shape_arr1 = ShapeArray.fromPolygon(p._points, scale = scale)
shape_arr2 = ShapeArray.fromPolygon(p_offset._points, scale = scale)
assert shape_arr1.arr.shape[0] >= (4 * scale) - 1 # -1 is to account for rounding errors
assert shape_arr2.arr.shape[0] >= (2 * offset + 4) * scale - 1