mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-06 22:47:29 -06:00
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:
commit
5704a7b184
41 changed files with 1109 additions and 341 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -40,6 +40,7 @@ plugins/cura-siemensnx-plugin
|
|||
plugins/CuraBlenderPlugin
|
||||
plugins/CuraCloudPlugin
|
||||
plugins/CuraDrivePlugin
|
||||
plugins/CuraDrive
|
||||
plugins/CuraLiveScriptingPlugin
|
||||
plugins/CuraOpenSCADPlugin
|
||||
plugins/CuraPrintProfileCreator
|
||||
|
|
32
cura/API/Backups.py
Normal file
32
cura/API/Backups.py
Normal 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
19
cura/API/__init__.py
Normal 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()
|
|
@ -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.Logger import Logger
|
||||
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.
|
||||
# Different priority schemes can be defined so it alters the behavior while using
|
||||
# the same logic.
|
||||
#
|
||||
# Note: Make sure the scale is the same between ShapeArray objects and the Arrange instance.
|
||||
class Arrange:
|
||||
build_volume = None
|
||||
|
||||
def __init__(self, x, y, offset_x, offset_y, scale= 1.0):
|
||||
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)
|
||||
def __init__(self, x, y, offset_x, offset_y, scale= 0.5):
|
||||
self._scale = scale # convert input coordinates to arrange coordinates
|
||||
self._offset_x = offset_x
|
||||
self._offset_y = offset_y
|
||||
world_x, world_y = int(x * self._scale), int(y * self._scale)
|
||||
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._is_empty = True
|
||||
|
||||
|
@ -39,7 +45,7 @@ class Arrange:
|
|||
# \param scene_root Root for finding all scene nodes
|
||||
# \param fixed_nodes Scene nodes to be placed
|
||||
@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.centerFirst()
|
||||
|
||||
|
@ -61,13 +67,17 @@ class Arrange:
|
|||
|
||||
# If a build volume was set, add the disallowed areas
|
||||
if Arrange.build_volume:
|
||||
disallowed_areas = Arrange.build_volume.getDisallowedAreas()
|
||||
disallowed_areas = Arrange.build_volume.getDisallowedAreasNoBrim()
|
||||
for area in disallowed_areas:
|
||||
points = copy.deepcopy(area._points)
|
||||
shape_arr = ShapeArray.fromPolygon(points, scale = scale)
|
||||
arranger.place(0, 0, shape_arr, update_empty = False)
|
||||
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)
|
||||
# return the nodes that should be placed
|
||||
# \param node
|
||||
|
@ -104,7 +114,7 @@ class Arrange:
|
|||
def centerFirst(self):
|
||||
# Square distance: creates a more round shape
|
||||
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.sort()
|
||||
|
||||
|
@ -112,7 +122,7 @@ class Arrange:
|
|||
# This is a strategy for the arranger.
|
||||
def backFirst(self):
|
||||
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.sort()
|
||||
|
||||
|
@ -126,9 +136,15 @@ class Arrange:
|
|||
y = int(self._scale * y)
|
||||
offset_x = x + self._offset_x + shape_arr.offset_x
|
||||
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[
|
||||
offset_y:offset_y + shape_arr.arr.shape[0],
|
||||
offset_x:offset_x + shape_arr.arr.shape[1]]
|
||||
offset_y:occupied_y_max,
|
||||
offset_x:occupied_x_max]
|
||||
try:
|
||||
if numpy.any(occupied_slice[numpy.where(shape_arr.arr == 1)]):
|
||||
return None
|
||||
|
@ -140,7 +156,7 @@ class Arrange:
|
|||
return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)])
|
||||
|
||||
## 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 start_prio Start with this priority value (and skip the ones before)
|
||||
# \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]:
|
||||
tryout_idx = numpy.where(self._priority == priority)
|
||||
for idx in range(len(tryout_idx[0])):
|
||||
x = tryout_idx[0][idx]
|
||||
y = tryout_idx[1][idx]
|
||||
projected_x = x - self._offset_x
|
||||
projected_y = y - self._offset_y
|
||||
x = tryout_idx[1][idx]
|
||||
y = tryout_idx[0][idx]
|
||||
projected_x = int((x - self._offset_x) / self._scale)
|
||||
projected_y = int((y - self._offset_y) / self._scale)
|
||||
|
||||
# array to "world" coordinates
|
||||
penalty_points = self.checkShape(projected_x, projected_y, shape_arr)
|
||||
if penalty_points is not None:
|
||||
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.
|
||||
prio_slice = self._priority[min_y:max_y, min_x:max_x]
|
||||
prio_slice[numpy.where(shape_arr.arr[
|
||||
min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 999
|
||||
prio_slice[new_occupied] = 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
|
||||
def isEmpty(self):
|
||||
|
|
|
@ -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.
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Job import Job
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Math.Vector import Vector
|
||||
|
@ -17,6 +18,7 @@ from cura.Arranging.ShapeArray import ShapeArray
|
|||
from typing import List
|
||||
|
||||
|
||||
## Do arrangements on multiple build plates (aka builtiplexer)
|
||||
class ArrangeArray:
|
||||
def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]):
|
||||
self._x = x
|
||||
|
@ -79,7 +81,11 @@ class ArrangeObjectsAllBuildPlatesJob(Job):
|
|||
nodes_arr.sort(key=lambda item: item[0])
|
||||
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.add()
|
||||
|
@ -93,27 +99,18 @@ class ArrangeObjectsAllBuildPlatesJob(Job):
|
|||
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,
|
||||
# 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
|
||||
|
||||
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:
|
||||
# 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():
|
||||
arrange_array.add()
|
||||
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
|
||||
node.removeDecorator(ZOffsetDecorator)
|
||||
if node.getBoundingBox():
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Job import Job
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Math.Vector import Vector
|
||||
|
@ -32,7 +33,11 @@ class ArrangeObjectsJob(Job):
|
|||
progress = 0,
|
||||
title = i18n_catalog.i18nc("@info:title", "Finding Location"))
|
||||
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
|
||||
nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr)
|
||||
|
@ -50,15 +55,15 @@ class ArrangeObjectsJob(Job):
|
|||
last_size = None
|
||||
grouped_operation = GroupedOperation()
|
||||
found_solution_for_all = True
|
||||
not_fit_count = 0
|
||||
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,
|
||||
# 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
|
||||
start_priority = last_priority
|
||||
else:
|
||||
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
|
||||
node.removeDecorator(ZOffsetDecorator)
|
||||
if node.getBoundingBox():
|
||||
|
@ -70,12 +75,12 @@ class ArrangeObjectsJob(Job):
|
|||
last_priority = best_spot.priority
|
||||
|
||||
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))
|
||||
else:
|
||||
Logger.log("d", "Arrange all: could not find spot!")
|
||||
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)
|
||||
Job.yieldThread()
|
||||
|
|
|
@ -74,7 +74,7 @@ class ShapeArray:
|
|||
# \param vertices
|
||||
@classmethod
|
||||
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
|
||||
|
||||
|
|
52
cura/AutoSave.py
Normal file
52
cura/AutoSave.py
Normal 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
155
cura/Backups/Backup.py
Normal 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
|
56
cura/Backups/BackupsManager.py
Normal file
56
cura/Backups/BackupsManager.py
Normal 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
0
cura/Backups/__init__.py
Normal file
|
@ -1,12 +1,8 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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.Scene.Platform import Platform
|
||||
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.Message import Message
|
||||
from UM.Signal import Signal
|
||||
from PyQt5.QtCore import QTimer
|
||||
from UM.View.RenderBatch import RenderBatch
|
||||
from UM.View.GL.OpenGL import OpenGL
|
||||
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
import numpy
|
||||
import math
|
||||
import copy
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
# Setting for clearance around the prime
|
||||
PRIME_CLEARANCE = 6.5
|
||||
|
||||
|
@ -63,6 +62,7 @@ class BuildVolume(SceneNode):
|
|||
self._grid_shader = None
|
||||
|
||||
self._disallowed_areas = []
|
||||
self._disallowed_areas_no_brim = []
|
||||
self._disallowed_area_mesh = None
|
||||
|
||||
self._error_areas = []
|
||||
|
@ -173,6 +173,9 @@ class BuildVolume(SceneNode):
|
|||
def getDisallowedAreas(self) -> List[Polygon]:
|
||||
return self._disallowed_areas
|
||||
|
||||
def getDisallowedAreasNoBrim(self) -> List[Polygon]:
|
||||
return self._disallowed_areas_no_brim
|
||||
|
||||
def setDisallowedAreas(self, areas: List[Polygon]):
|
||||
self._disallowed_areas = areas
|
||||
|
||||
|
@ -457,7 +460,7 @@ class BuildVolume(SceneNode):
|
|||
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))
|
||||
|
||||
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.
|
||||
# This is probably wrong in all other cases. TODO!
|
||||
|
@ -649,7 +652,7 @@ class BuildVolume(SceneNode):
|
|||
|
||||
extruder_manager = ExtruderManager.getInstance()
|
||||
used_extruders = extruder_manager.getUsedExtruderStacks()
|
||||
disallowed_border_size = self._getEdgeDisallowedSize()
|
||||
disallowed_border_size = self.getEdgeDisallowedSize()
|
||||
|
||||
if not used_extruders:
|
||||
# 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.
|
||||
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.
|
||||
for extruder in used_extruders:
|
||||
|
@ -689,12 +693,15 @@ class BuildVolume(SceneNode):
|
|||
break
|
||||
|
||||
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")
|
||||
for area in nozzle_disallowed_areas:
|
||||
polygon = Polygon(numpy.array(area, numpy.float32))
|
||||
polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size))
|
||||
result_areas[extruder_id].append(polygon) #Don't perform the offset on these.
|
||||
polygon_disallowed_border = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size))
|
||||
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.
|
||||
if len(used_extruders) > 1: #No prime tower in single-extrusion.
|
||||
|
@ -710,6 +717,7 @@ class BuildVolume(SceneNode):
|
|||
break
|
||||
if not prime_tower_collision:
|
||||
result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
|
||||
result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
|
||||
else:
|
||||
self._error_areas.extend(prime_tower_areas[extruder_id])
|
||||
|
||||
|
@ -718,6 +726,9 @@ class BuildVolume(SceneNode):
|
|||
self._disallowed_areas = []
|
||||
for extruder_id in result_areas:
|
||||
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
|
||||
# features.
|
||||
|
@ -951,12 +962,12 @@ class BuildVolume(SceneNode):
|
|||
all_values[i] = 0
|
||||
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
|
||||
# not part of the collision radius, such as bed adhesion (skirt/brim/raft)
|
||||
# and travel avoid distance.
|
||||
def _getEdgeDisallowedSize(self):
|
||||
def getEdgeDisallowedSize(self):
|
||||
if not self._global_container_stack or not self._global_container_stack.extruders:
|
||||
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"]
|
||||
_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"]
|
||||
_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.
|
||||
_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"]
|
||||
|
|
|
@ -72,7 +72,8 @@ class CuraActions(QObject):
|
|||
# \param count The number of times to multiply the selection.
|
||||
@pyqtSlot(int)
|
||||
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()
|
||||
|
||||
## Delete all selected objects.
|
||||
|
|
|
@ -85,6 +85,7 @@ from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
|
|||
from cura.Machines.VariantManager import VariantManager
|
||||
|
||||
from .SingleInstance import SingleInstance
|
||||
from .AutoSave import AutoSave
|
||||
from . import PlatformPhysics
|
||||
from . import BuildVolume
|
||||
from . import CameraAnimation
|
||||
|
@ -154,9 +155,6 @@ class CuraApplication(QtApplication):
|
|||
|
||||
self._boot_loading_time = time.time()
|
||||
|
||||
self._currently_loading_files = []
|
||||
self._non_sliceable_extensions = []
|
||||
|
||||
# Variables set from CLI
|
||||
self._files_to_open = []
|
||||
self._use_single_instance = False
|
||||
|
@ -223,6 +221,10 @@ class CuraApplication(QtApplication):
|
|||
|
||||
self._need_to_show_user_agreement = True
|
||||
|
||||
# Backups
|
||||
self._auto_save = None
|
||||
self._save_data_enabled = True
|
||||
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
self._container_registry_class = CuraContainerRegistry
|
||||
|
||||
|
@ -469,6 +471,7 @@ class CuraApplication(QtApplication):
|
|||
|
||||
preferences.addPreference("cura/categories_expanded", "")
|
||||
preferences.addPreference("cura/jobname_prefix", True)
|
||||
preferences.addPreference("cura/select_models_on_load", False)
|
||||
preferences.addPreference("view/center_on_select", False)
|
||||
preferences.addPreference("mesh/scale_to_fit", False)
|
||||
preferences.addPreference("mesh/scale_tiny_meshes", True)
|
||||
|
@ -585,14 +588,17 @@ class CuraApplication(QtApplication):
|
|||
|
||||
showPrintMonitor = pyqtSignal(bool, arguments = ["show"])
|
||||
|
||||
## Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
|
||||
#
|
||||
# Note that the AutoSave plugin also calls this method.
|
||||
def saveSettings(self):
|
||||
if not self.started: # Do not do saving during application start
|
||||
return
|
||||
def setSaveDataEnabled(self, enabled: bool) -> None:
|
||||
self._save_data_enabled = enabled
|
||||
|
||||
# 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()
|
||||
Preferences.getInstance().writeToFile(Resources.getStoragePath(Resources.Preferences,
|
||||
self._application_name + ".cfg"))
|
||||
|
||||
def saveStack(self, stack):
|
||||
ContainerRegistry.getInstance().saveContainer(stack)
|
||||
|
@ -695,6 +701,9 @@ class CuraApplication(QtApplication):
|
|||
self._post_start_timer.timeout.connect(self._onPostStart)
|
||||
self._post_start_timer.start()
|
||||
|
||||
self._auto_save = AutoSave(self)
|
||||
self._auto_save.initialize()
|
||||
|
||||
self.exec_()
|
||||
|
||||
def __setUpSingleInstanceServer(self):
|
||||
|
@ -844,6 +853,9 @@ class CuraApplication(QtApplication):
|
|||
|
||||
return super().event(event)
|
||||
|
||||
def getAutoSave(self):
|
||||
return self._auto_save
|
||||
|
||||
## Get print information (duration / material used)
|
||||
def getPrintInformation(self):
|
||||
return self._print_information
|
||||
|
@ -1228,34 +1240,12 @@ class CuraApplication(QtApplication):
|
|||
nodes.append(node)
|
||||
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
|
||||
# \param nodes nodes that we have to place
|
||||
# \param fixed_nodes nodes that are placed in the arranger before finding spots for 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()
|
||||
|
||||
## Reload all mesh data on the screen from file.
|
||||
|
@ -1539,6 +1529,9 @@ class CuraApplication(QtApplication):
|
|||
self.callLater(self.openProjectFile.emit, file)
|
||||
return
|
||||
|
||||
if Preferences.getInstance().getValue("cura/select_models_on_load"):
|
||||
Selection.clear()
|
||||
|
||||
f = file.toLocalFile()
|
||||
extension = os.path.splitext(f)[1]
|
||||
filename = os.path.basename(f)
|
||||
|
@ -1585,11 +1578,16 @@ class CuraApplication(QtApplication):
|
|||
for node_ in DepthFirstIterator(root):
|
||||
if node_.callDecoration("isSliceable") and node_.callDecoration("getBuildPlateNumber") == target_build_plate:
|
||||
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
|
||||
default_extruder_position = self.getMachineManager().defaultExtruderPosition
|
||||
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:
|
||||
|
||||
# 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)):
|
||||
node.scale(original_node.getScale())
|
||||
|
||||
|
||||
node.setSelectable(True)
|
||||
node.setName(os.path.basename(filename))
|
||||
self.getBuildVolume().checkBoundsAndUpdate(node)
|
||||
|
@ -1663,6 +1660,9 @@ class CuraApplication(QtApplication):
|
|||
node.callDecoration("setActiveExtruder", default_extruder_id)
|
||||
scene.sceneChanged.emit(node)
|
||||
|
||||
if select_models_on_load:
|
||||
Selection.add(node)
|
||||
|
||||
self.fileCompleted.emit(filename)
|
||||
|
||||
def addNonSliceableExtension(self, extension):
|
||||
|
|
|
@ -30,11 +30,18 @@ class MultiplyObjectsJob(Job):
|
|||
total_progress = len(self._objects) * self._count
|
||||
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()
|
||||
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 = []
|
||||
nodes = []
|
||||
|
||||
not_fit_count = 0
|
||||
|
||||
for node in self._objects:
|
||||
# If object is part of a group, multiply group
|
||||
current_node = node
|
||||
|
@ -46,12 +53,13 @@ class MultiplyObjectsJob(Job):
|
|||
processed_nodes.append(current_node)
|
||||
|
||||
node_too_big = False
|
||||
if node.getBoundingBox().width < 300 or node.getBoundingBox().depth < 300:
|
||||
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset=self._min_offset)
|
||||
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, scale = scale)
|
||||
else:
|
||||
node_too_big = True
|
||||
|
||||
found_solution_for_all = True
|
||||
arranger.resetLastPriority()
|
||||
for i in range(self._count):
|
||||
# We do place the nodes one by one, as we want to yield in between.
|
||||
if not node_too_big:
|
||||
|
@ -59,8 +67,9 @@ class MultiplyObjectsJob(Job):
|
|||
if node_too_big or not solution_found:
|
||||
found_solution_for_all = False
|
||||
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)
|
||||
not_fit_count += 1
|
||||
|
||||
# Same build plate
|
||||
build_plate_number = current_node.callDecoration("getBuildPlateNumber")
|
||||
|
|
|
@ -79,10 +79,10 @@ class PreviewPass(RenderPass):
|
|||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
|
||||
per_mesh_stack = node.callDecoration("getStack")
|
||||
if node.callDecoration("isNonPrintingMesh"):
|
||||
if node.callDecoration("isNonThumbnailVisibleMesh"):
|
||||
# Non printing mesh
|
||||
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
|
||||
uniforms = {}
|
||||
shade_factor = 0.6
|
||||
|
@ -112,4 +112,3 @@ class PreviewPass(RenderPass):
|
|||
batch_support_mesh.render(render_camera)
|
||||
|
||||
self.release()
|
||||
|
||||
|
|
|
@ -279,9 +279,12 @@ class PrintInformation(QObject):
|
|||
for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
|
||||
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)
|
||||
def setJobName(self, name):
|
||||
self._job_name = name
|
||||
self._base_name = name.replace(self._abbr_machine + "_", "")
|
||||
self.jobNameChanged.emit()
|
||||
|
||||
jobNameChanged = pyqtSignal()
|
||||
|
|
|
@ -30,6 +30,7 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
|||
# Note that Support Mesh is not in here because it actually generates
|
||||
# g-code in the volume of the 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):
|
||||
super().__init__()
|
||||
|
@ -41,6 +42,7 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
|||
self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0).getId()
|
||||
|
||||
self._is_non_printing_mesh = False
|
||||
self._is_non_thumbnail_visible_mesh = False
|
||||
|
||||
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"
|
||||
# has not been updated yet.
|
||||
deep_copy._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
|
||||
deep_copy._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh()
|
||||
|
||||
return deep_copy
|
||||
|
||||
|
@ -102,10 +105,17 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
|||
def evaluateIsNonPrintingMesh(self):
|
||||
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
|
||||
if property_name == "value":
|
||||
# Trigger slice/need slicing if the value has changed.
|
||||
self._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
|
||||
self._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh()
|
||||
|
||||
Application.getInstance().getBackend().needsSlicing()
|
||||
Application.getInstance().getBackend().tickle()
|
||||
|
|
|
@ -48,7 +48,7 @@ class Snapshot:
|
|||
# determine zoom and look at
|
||||
bbox = None
|
||||
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:
|
||||
bbox = node.getBoundingBox()
|
||||
else:
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# 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
|
||||
|
||||
|
||||
class CuraStage(Stage):
|
||||
|
||||
def __init__(self, parent = None):
|
||||
|
|
|
@ -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
|
|
@ -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() }
|
|
@ -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"
|
||||
}
|
|
@ -9,7 +9,7 @@ from . import GCodeGzWriter
|
|||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
file_extension = "gz" if Platform.isOSX() else "gcode.gz"
|
||||
file_extension = "gcode.gz"
|
||||
return {
|
||||
"mesh_writer": {
|
||||
"output": [{
|
||||
|
|
|
@ -69,10 +69,11 @@ class MonitorStage(CuraStage):
|
|||
self._updateSidebar()
|
||||
|
||||
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)
|
||||
|
||||
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.qml")
|
||||
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles),
|
||||
"MonitorSidebar.qml")
|
||||
self.addDisplayComponent("sidebar", sidebar_component_path)
|
||||
|
|
|
@ -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):
|
||||
|
||||
"""data is a list. Each index contains a layer"""
|
||||
|
||||
x = 0.
|
||||
y = 0.
|
||||
pause_at = self.getSettingValueByKey("pause_at")
|
||||
pause_height = self.getSettingValueByKey("pause_height")
|
||||
pause_layer = self.getSettingValueByKey("pause_layer")
|
||||
|
@ -138,73 +147,94 @@ class PauseAtHeight(Script):
|
|||
resume_temperature = self.getSettingValueByKey("resume_temperature")
|
||||
|
||||
# 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>
|
||||
layer_0_z = 0.
|
||||
current_z = 0
|
||||
got_first_g_cmd_on_layer_0 = False
|
||||
|
||||
nbr_negative_layers = 0
|
||||
|
||||
for index, layer in enumerate(data):
|
||||
lines = layer.split("\n")
|
||||
|
||||
# Scroll each line of instruction for each layer in the G-code
|
||||
for line in lines:
|
||||
# Fist positive layer reached
|
||||
if ";LAYER:0" in line:
|
||||
layers_started = True
|
||||
# Count nbr of negative layers (raft)
|
||||
elif ";LAYER:-" in line:
|
||||
nbr_negative_layers += 1
|
||||
if not layers_started:
|
||||
continue
|
||||
|
||||
# If a Z instruction is in the line, read the current Z
|
||||
if self.getValue(line, "Z") is not None:
|
||||
current_z = self.getValue(line, "Z")
|
||||
|
||||
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:
|
||||
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:
|
||||
layer_0_z = current_z
|
||||
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
|
||||
|
||||
if current_height < pause_height:
|
||||
break #Try the next layer.
|
||||
else: #Pause at layer.
|
||||
break # Try the next layer.
|
||||
|
||||
# Pause at layer
|
||||
else:
|
||||
if not line.startswith(";LAYER:"):
|
||||
continue
|
||||
current_layer = line[len(";LAYER:"):]
|
||||
try:
|
||||
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]
|
||||
prevLines = prevLayer.split("\n")
|
||||
# Couldn't cast to int. Something is wrong with this
|
||||
# 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.
|
||||
|
||||
# Access last layer, browse it backwards to find
|
||||
# last extruder absolute position
|
||||
for prevLine in reversed(prevLines):
|
||||
for prevLine in reversed(prev_lines):
|
||||
current_e = self.getValue(prevLine, "E", -1)
|
||||
if current_e >= 0:
|
||||
break
|
||||
|
||||
# include a number of previous layers
|
||||
for i in range(1, redo_layers + 1):
|
||||
prevLayer = data[index - i]
|
||||
layer = prevLayer + layer
|
||||
prev_layer = data[index - i]
|
||||
layer = prev_layer + layer
|
||||
|
||||
# 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
|
||||
if i == redo_layers:
|
||||
prevLines = prevLayer.split("\n")
|
||||
for line in prevLines:
|
||||
# Get X and Y from the next layer (better position for
|
||||
# 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)
|
||||
|
||||
if new_e != current_e:
|
||||
current_e = new_e
|
||||
break
|
||||
|
@ -213,61 +243,63 @@ class PauseAtHeight(Script):
|
|||
prepend_gcode += ";added code by post processing\n"
|
||||
prepend_gcode += ";script: PauseAtHeight.py\n"
|
||||
if pause_at == "height":
|
||||
prepend_gcode += ";current z: {z}\n".format(z = current_z)
|
||||
prepend_gcode += ";current height: {height}\n".format(height = current_height)
|
||||
prepend_gcode += ";current z: {z}\n".format(z=current_z)
|
||||
prepend_gcode += ";current height: {height}\n".format(height=current_height)
|
||||
else:
|
||||
prepend_gcode += ";current layer: {layer}\n".format(layer = current_layer)
|
||||
prepend_gcode += ";current layer: {layer}\n".format(layer=current_layer)
|
||||
|
||||
# Retraction
|
||||
prepend_gcode += self.putValue(M = 83) + "\n"
|
||||
prepend_gcode += self.putValue(M=83) + "\n"
|
||||
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
|
||||
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"
|
||||
prepend_gcode += self.putValue(G=1, Z=current_z + 1, F=300) + "\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:
|
||||
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
|
||||
prepend_gcode += self.putValue(M = 84, E = 0) + "\n"
|
||||
prepend_gcode += self.putValue(M=84, E=0) + "\n"
|
||||
|
||||
# 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
|
||||
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
|
||||
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,
|
||||
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
|
||||
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
|
||||
# when changing filament.
|
||||
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
|
||||
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, Z=current_z + 1, F=300) + "\n"
|
||||
prepend_gcode += self.putValue(G=1, X=x, Y=y, F=9000) + "\n"
|
||||
if retraction_amount != 0:
|
||||
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(M = 82) + "\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(M=82) + "\n"
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
# Override the data of this layer with the
|
||||
# modified data
|
||||
data[index] = layer
|
||||
|
|
|
@ -14,5 +14,6 @@ class PrepareStage(CuraStage):
|
|||
Application.getInstance().engineCreatedSignal.connect(self._engineCreated)
|
||||
|
||||
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)
|
||||
|
|
|
@ -24,17 +24,26 @@ from .PackagesModel import PackagesModel
|
|||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## The Toolbox class is responsible of communicating with the server through the API
|
||||
class Toolbox(QObject, Extension):
|
||||
|
||||
DEFAULT_PACKAGES_API_ROOT = "https://api.ultimaker.com"
|
||||
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self._application = Application.getInstance()
|
||||
self._package_manager = None
|
||||
self._plugin_registry = Application.getInstance().getPluginRegistry()
|
||||
self._packages_api_root = self._getPackagesApiRoot()
|
||||
self._packages_version = self._getPackagesVersion()
|
||||
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:
|
||||
self._get_packages_request = None
|
||||
|
@ -153,6 +162,15 @@ class Toolbox(QObject, Extension):
|
|||
def _onAppInitialized(self) -> None:
|
||||
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:
|
||||
if not hasattr(cura, "CuraVersion"):
|
||||
return self._plugin_registry.APIVersion
|
||||
|
|
|
@ -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
|
||||
extruder_temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line)
|
||||
# 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]:
|
||||
extruder.updateHotendTemperature(float(match[1]))
|
||||
if match[2]:
|
||||
|
|
|
@ -60,7 +60,7 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
|
|||
self._check_updates = True
|
||||
self._update_thread.start()
|
||||
|
||||
def stop(self):
|
||||
def stop(self, store_data: bool = True):
|
||||
self._check_updates = False
|
||||
|
||||
def _onConnectionStateChanged(self, serial_port):
|
||||
|
@ -79,10 +79,11 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
|
|||
if container_stack is None:
|
||||
time.sleep(5)
|
||||
continue
|
||||
port_list = [] # Just an empty list; all USB devices will be removed.
|
||||
if container_stack.getMetaDataEntry("supports_usb_connection"):
|
||||
machine_file_formats = [file_type.strip() for file_type in container_stack.getMetaDataEntry("file_formats").split(";")]
|
||||
if "text/x-gcode" in machine_file_formats:
|
||||
port_list = self.getSerialPortList(only_list_usb=True)
|
||||
else:
|
||||
port_list = [] # Just use an empty list; all USB devices will be removed.
|
||||
self._addRemovePorts(port_list)
|
||||
time.sleep(5)
|
||||
|
||||
|
|
|
@ -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": {
|
||||
"package_info": {
|
||||
"package_id": "ChangeLogPlugin",
|
||||
|
|
|
@ -3324,6 +3324,16 @@
|
|||
"settable_per_mesh": false,
|
||||
"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":
|
||||
{
|
||||
"label": "Travel Avoid Distance",
|
||||
|
|
|
@ -3106,6 +3106,18 @@ msgid ""
|
|||
"available when combing is enabled."
|
||||
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
|
||||
msgctxt "travel_avoid_distance label"
|
||||
msgid "Travel Avoid Distance"
|
||||
|
|
|
@ -15,6 +15,8 @@ Item
|
|||
id: base;
|
||||
UM.I18nCatalog { id: catalog; name:"cura"}
|
||||
|
||||
height: childrenRect.height + UM.Theme.getSize("sidebar_margin").height
|
||||
|
||||
property bool printerConnected: Cura.MachineManager.printerConnected
|
||||
property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands
|
||||
property var activePrinter: printerConnected ? Cura.MachineManager.printerOutputDevices[0].activePrinter : null
|
||||
|
|
211
resources/qml/MonitorSidebar.qml
Normal file
211
resources/qml/MonitorSidebar.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -79,6 +79,8 @@ UM.PreferencesPage
|
|||
scaleToFitCheckbox.checked = boolCheck(UM.Preferences.getValue("mesh/scale_to_fit"))
|
||||
UM.Preferences.resetPreference("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")
|
||||
prefixJobNameCheckbox.checked = boolCheck(UM.Preferences.getValue("cura/jobname_prefix"))
|
||||
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
|
||||
{
|
||||
width: childrenRect.width
|
||||
|
|
|
@ -24,8 +24,6 @@ Rectangle
|
|||
property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands
|
||||
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 printMaterialLengths: PrintInformation.materialLengths
|
||||
property variant printMaterialWeights: PrintInformation.materialWeights
|
||||
|
@ -120,7 +118,7 @@ Rectangle
|
|||
SidebarHeader {
|
||||
id: header
|
||||
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
|
||||
|
||||
onShowTooltip: base.showTooltip(item, location, text)
|
||||
|
@ -158,7 +156,7 @@ Rectangle
|
|||
width: Math.round(parent.width * 0.45)
|
||||
font: UM.Theme.getFont("large")
|
||||
color: UM.Theme.getColor("text")
|
||||
visible: !monitoringPrint && !hideView
|
||||
visible: !hideView
|
||||
}
|
||||
|
||||
// Settings mode selection toggle
|
||||
|
@ -185,7 +183,7 @@ Rectangle
|
|||
}
|
||||
}
|
||||
|
||||
visible: !monitoringPrint && !hideSettings && !hideView
|
||||
visible: !hideSettings && !hideView
|
||||
|
||||
Component
|
||||
{
|
||||
|
@ -282,7 +280,7 @@ Rectangle
|
|||
anchors.topMargin: UM.Theme.getSize("sidebar_margin").height
|
||||
anchors.left: base.left
|
||||
anchors.right: base.right
|
||||
visible: !monitoringPrint && !hideSettings
|
||||
visible: !hideSettings
|
||||
|
||||
replaceEnter: Transition {
|
||||
PropertyAnimation {
|
||||
|
@ -305,47 +303,11 @@ Rectangle
|
|||
|
||||
Loader
|
||||
{
|
||||
id: controlItem
|
||||
anchors.bottom: footerSeparator.top
|
||||
anchors.top: monitoringPrint ? machineSelection.bottom : headerSeparator.bottom
|
||||
anchors.top: headerSeparator.bottom
|
||||
anchors.left: base.left
|
||||
anchors.right: base.right
|
||||
sourceComponent:
|
||||
{
|
||||
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 ""
|
||||
}
|
||||
}
|
||||
source: "SidebarContents.qml"
|
||||
}
|
||||
|
||||
Rectangle
|
||||
|
@ -367,7 +329,6 @@ Rectangle
|
|||
anchors.bottomMargin: UM.Theme.getSize("sidebar_margin").height
|
||||
height: timeDetails.height + costSpec.height
|
||||
width: base.width - (saveButton.buttonRowWidth + UM.Theme.getSize("sidebar_margin").width)
|
||||
visible: !monitoringPrint
|
||||
clip: true
|
||||
|
||||
Label
|
||||
|
@ -570,8 +531,7 @@ Rectangle
|
|||
}
|
||||
}
|
||||
|
||||
// SaveButton and MonitorButton are actually the bottom footer panels.
|
||||
// "!monitoringPrint" currently means "show-settings-mode"
|
||||
// SaveButton is actually the bottom footer panel.
|
||||
SaveButton
|
||||
{
|
||||
id: saveButton
|
||||
|
@ -579,17 +539,6 @@ Rectangle
|
|||
anchors.top: footerSeparator.bottom
|
||||
anchors.topMargin: UM.Theme.getSize("sidebar_margin").height
|
||||
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
|
|
@ -17,7 +17,17 @@ Column
|
|||
property int currentExtruderIndex: Cura.ExtruderManager.activeExtruderIndex;
|
||||
property bool currentExtruderVisible: extrudersList.visible;
|
||||
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 buildplateCompatibilityWarning: Cura.MachineManager.variantBuildplateUsable
|
||||
|
||||
|
|
|
@ -72,6 +72,7 @@ jerk_enabled
|
|||
[travel]
|
||||
retraction_combing
|
||||
travel_avoid_other_parts
|
||||
travel_avoid_supports
|
||||
travel_avoid_distance
|
||||
retraction_hop_enabled
|
||||
retraction_hop_only_when_collides
|
||||
|
|
|
@ -187,6 +187,7 @@ jerk_skirt_brim
|
|||
retraction_combing
|
||||
travel_retract_before_outer_wall
|
||||
travel_avoid_other_parts
|
||||
travel_avoid_supports
|
||||
travel_avoid_distance
|
||||
start_layers_at_same_position
|
||||
layer_start_x
|
||||
|
|
|
@ -4,9 +4,27 @@ from cura.Arranging.Arrange import Arrange
|
|||
from cura.Arranging.ShapeArray import ShapeArray
|
||||
|
||||
|
||||
def gimmeShapeArray():
|
||||
vertices = numpy.array([[-3, 1], [3, 1], [0, -3]])
|
||||
shape_arr = ShapeArray.fromPolygon(vertices)
|
||||
## Triangle of area 12
|
||||
def gimmeTriangle():
|
||||
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
|
||||
|
||||
|
||||
|
@ -20,9 +38,48 @@ def test_smoke_ShapeArray():
|
|||
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
|
||||
def test_centerFirst():
|
||||
ar = Arrange(300, 300, 150, 150)
|
||||
ar = Arrange(300, 300, 150, 150, scale = 1)
|
||||
ar.centerFirst()
|
||||
assert ar._priority[150][150] < ar._priority[170][150]
|
||||
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]
|
||||
|
||||
|
||||
## 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
|
||||
def test_backFirst():
|
||||
ar = Arrange(300, 300, 150, 150)
|
||||
ar = Arrange(300, 300, 150, 150, scale = 1)
|
||||
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[150][130]
|
||||
assert ar._priority[150][150] > ar._priority[130][150]
|
||||
assert ar._priority[150][150] > ar._priority[130][130]
|
||||
|
||||
|
||||
## See if the result of bestSpot has the correct form
|
||||
def test_smoke_bestSpot():
|
||||
ar = Arrange(30, 30, 15, 15)
|
||||
ar = Arrange(30, 30, 15, 15, scale = 1)
|
||||
ar.centerFirst()
|
||||
|
||||
shape_arr = gimmeShapeArray()
|
||||
|
@ -55,6 +132,113 @@ def test_smoke_bestSpot():
|
|||
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
|
||||
def test_smoke_place():
|
||||
ar = Arrange(30, 30, 15, 15)
|
||||
|
@ -80,6 +264,20 @@ def test_checkShape():
|
|||
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.
|
||||
def test_checkShape_place():
|
||||
ar = Arrange(30, 30, 15, 15)
|
||||
|
@ -95,7 +293,7 @@ def test_checkShape_place():
|
|||
|
||||
## Test the whole sequence
|
||||
def test_smoke_place_objects():
|
||||
ar = Arrange(20, 20, 10, 10)
|
||||
ar = Arrange(20, 20, 10, 10, scale = 1)
|
||||
ar.centerFirst()
|
||||
shape_arr = gimmeShapeArray()
|
||||
|
||||
|
@ -104,6 +302,13 @@ def test_smoke_place_objects():
|
|||
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
|
||||
def test_arrayFromPolygon():
|
||||
vertices = numpy.array([[-3, 1], [3, 1], [0, -3]])
|
||||
|
@ -145,3 +350,30 @@ def test_check2():
|
|||
assert numpy.any(check_array)
|
||||
assert not check_array[3][0]
|
||||
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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue