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/CuraBlenderPlugin
|
||||||
plugins/CuraCloudPlugin
|
plugins/CuraCloudPlugin
|
||||||
plugins/CuraDrivePlugin
|
plugins/CuraDrivePlugin
|
||||||
|
plugins/CuraDrive
|
||||||
plugins/CuraLiveScriptingPlugin
|
plugins/CuraLiveScriptingPlugin
|
||||||
plugins/CuraOpenSCADPlugin
|
plugins/CuraOpenSCADPlugin
|
||||||
plugins/CuraPrintProfileCreator
|
plugins/CuraPrintProfileCreator
|
||||||
|
|
32
cura/API/Backups.py
Normal file
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.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
|
@ -18,17 +21,20 @@ LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points
|
||||||
# good locations for objects that you try to put on a build place.
|
# good locations for objects that you try to put on a build place.
|
||||||
# Different priority schemes can be defined so it alters the behavior while using
|
# Different priority schemes can be defined so it alters the behavior while using
|
||||||
# the same logic.
|
# the same logic.
|
||||||
|
#
|
||||||
|
# Note: Make sure the scale is the same between ShapeArray objects and the Arrange instance.
|
||||||
class Arrange:
|
class Arrange:
|
||||||
build_volume = None
|
build_volume = None
|
||||||
|
|
||||||
def __init__(self, x, y, offset_x, offset_y, scale= 1.0):
|
def __init__(self, x, y, offset_x, offset_y, scale= 0.5):
|
||||||
self.shape = (y, x)
|
|
||||||
self._priority = numpy.zeros((x, y), dtype=numpy.int32)
|
|
||||||
self._priority_unique_values = []
|
|
||||||
self._occupied = numpy.zeros((x, y), dtype=numpy.int32)
|
|
||||||
self._scale = scale # convert input coordinates to arrange coordinates
|
self._scale = scale # convert input coordinates to arrange coordinates
|
||||||
self._offset_x = offset_x
|
world_x, world_y = int(x * self._scale), int(y * self._scale)
|
||||||
self._offset_y = offset_y
|
self._shape = (world_y, world_x)
|
||||||
|
self._priority = numpy.zeros((world_y, world_x), dtype=numpy.int32) # beware: these are indexed (y, x)
|
||||||
|
self._priority_unique_values = []
|
||||||
|
self._occupied = numpy.zeros((world_y, world_x), dtype=numpy.int32) # beware: these are indexed (y, x)
|
||||||
|
self._offset_x = int(offset_x * self._scale)
|
||||||
|
self._offset_y = int(offset_y * self._scale)
|
||||||
self._last_priority = 0
|
self._last_priority = 0
|
||||||
self._is_empty = True
|
self._is_empty = True
|
||||||
|
|
||||||
|
@ -39,7 +45,7 @@ class Arrange:
|
||||||
# \param scene_root Root for finding all scene nodes
|
# \param scene_root Root for finding all scene nodes
|
||||||
# \param fixed_nodes Scene nodes to be placed
|
# \param fixed_nodes Scene nodes to be placed
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 220, y = 220):
|
def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250):
|
||||||
arranger = Arrange(x, y, x // 2, y // 2, scale = scale)
|
arranger = Arrange(x, y, x // 2, y // 2, scale = scale)
|
||||||
arranger.centerFirst()
|
arranger.centerFirst()
|
||||||
|
|
||||||
|
@ -61,13 +67,17 @@ class Arrange:
|
||||||
|
|
||||||
# If a build volume was set, add the disallowed areas
|
# If a build volume was set, add the disallowed areas
|
||||||
if Arrange.build_volume:
|
if Arrange.build_volume:
|
||||||
disallowed_areas = Arrange.build_volume.getDisallowedAreas()
|
disallowed_areas = Arrange.build_volume.getDisallowedAreasNoBrim()
|
||||||
for area in disallowed_areas:
|
for area in disallowed_areas:
|
||||||
points = copy.deepcopy(area._points)
|
points = copy.deepcopy(area._points)
|
||||||
shape_arr = ShapeArray.fromPolygon(points, scale = scale)
|
shape_arr = ShapeArray.fromPolygon(points, scale = scale)
|
||||||
arranger.place(0, 0, shape_arr, update_empty = False)
|
arranger.place(0, 0, shape_arr, update_empty = False)
|
||||||
return arranger
|
return arranger
|
||||||
|
|
||||||
|
## This resets the optimization for finding location based on size
|
||||||
|
def resetLastPriority(self):
|
||||||
|
self._last_priority = 0
|
||||||
|
|
||||||
## Find placement for a node (using offset shape) and place it (using hull shape)
|
## Find placement for a node (using offset shape) and place it (using hull shape)
|
||||||
# return the nodes that should be placed
|
# return the nodes that should be placed
|
||||||
# \param node
|
# \param node
|
||||||
|
@ -104,7 +114,7 @@ class Arrange:
|
||||||
def centerFirst(self):
|
def centerFirst(self):
|
||||||
# Square distance: creates a more round shape
|
# Square distance: creates a more round shape
|
||||||
self._priority = numpy.fromfunction(
|
self._priority = numpy.fromfunction(
|
||||||
lambda i, j: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self.shape, dtype=numpy.int32)
|
lambda j, i: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self._shape, dtype=numpy.int32)
|
||||||
self._priority_unique_values = numpy.unique(self._priority)
|
self._priority_unique_values = numpy.unique(self._priority)
|
||||||
self._priority_unique_values.sort()
|
self._priority_unique_values.sort()
|
||||||
|
|
||||||
|
@ -112,7 +122,7 @@ class Arrange:
|
||||||
# This is a strategy for the arranger.
|
# This is a strategy for the arranger.
|
||||||
def backFirst(self):
|
def backFirst(self):
|
||||||
self._priority = numpy.fromfunction(
|
self._priority = numpy.fromfunction(
|
||||||
lambda i, j: 10 * j + abs(self._offset_x - i), self.shape, dtype=numpy.int32)
|
lambda j, i: 10 * j + abs(self._offset_x - i), self._shape, dtype=numpy.int32)
|
||||||
self._priority_unique_values = numpy.unique(self._priority)
|
self._priority_unique_values = numpy.unique(self._priority)
|
||||||
self._priority_unique_values.sort()
|
self._priority_unique_values.sort()
|
||||||
|
|
||||||
|
@ -126,9 +136,15 @@ class Arrange:
|
||||||
y = int(self._scale * y)
|
y = int(self._scale * y)
|
||||||
offset_x = x + self._offset_x + shape_arr.offset_x
|
offset_x = x + self._offset_x + shape_arr.offset_x
|
||||||
offset_y = y + self._offset_y + shape_arr.offset_y
|
offset_y = y + self._offset_y + shape_arr.offset_y
|
||||||
|
if offset_x < 0 or offset_y < 0:
|
||||||
|
return None # out of bounds in self._occupied
|
||||||
|
occupied_x_max = offset_x + shape_arr.arr.shape[1]
|
||||||
|
occupied_y_max = offset_y + shape_arr.arr.shape[0]
|
||||||
|
if occupied_x_max > self._occupied.shape[1] + 1 or occupied_y_max > self._occupied.shape[0] + 1:
|
||||||
|
return None # out of bounds in self._occupied
|
||||||
occupied_slice = self._occupied[
|
occupied_slice = self._occupied[
|
||||||
offset_y:offset_y + shape_arr.arr.shape[0],
|
offset_y:occupied_y_max,
|
||||||
offset_x:offset_x + shape_arr.arr.shape[1]]
|
offset_x:occupied_x_max]
|
||||||
try:
|
try:
|
||||||
if numpy.any(occupied_slice[numpy.where(shape_arr.arr == 1)]):
|
if numpy.any(occupied_slice[numpy.where(shape_arr.arr == 1)]):
|
||||||
return None
|
return None
|
||||||
|
@ -140,7 +156,7 @@ class Arrange:
|
||||||
return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)])
|
return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)])
|
||||||
|
|
||||||
## Find "best" spot for ShapeArray
|
## Find "best" spot for ShapeArray
|
||||||
# Return namedtuple with properties x, y, penalty_points, priority
|
# Return namedtuple with properties x, y, penalty_points, priority.
|
||||||
# \param shape_arr ShapeArray
|
# \param shape_arr ShapeArray
|
||||||
# \param start_prio Start with this priority value (and skip the ones before)
|
# \param start_prio Start with this priority value (and skip the ones before)
|
||||||
# \param step Slicing value, higher = more skips = faster but less accurate
|
# \param step Slicing value, higher = more skips = faster but less accurate
|
||||||
|
@ -153,12 +169,11 @@ class Arrange:
|
||||||
for priority in self._priority_unique_values[start_idx::step]:
|
for priority in self._priority_unique_values[start_idx::step]:
|
||||||
tryout_idx = numpy.where(self._priority == priority)
|
tryout_idx = numpy.where(self._priority == priority)
|
||||||
for idx in range(len(tryout_idx[0])):
|
for idx in range(len(tryout_idx[0])):
|
||||||
x = tryout_idx[0][idx]
|
x = tryout_idx[1][idx]
|
||||||
y = tryout_idx[1][idx]
|
y = tryout_idx[0][idx]
|
||||||
projected_x = x - self._offset_x
|
projected_x = int((x - self._offset_x) / self._scale)
|
||||||
projected_y = y - self._offset_y
|
projected_y = int((y - self._offset_y) / self._scale)
|
||||||
|
|
||||||
# array to "world" coordinates
|
|
||||||
penalty_points = self.checkShape(projected_x, projected_y, shape_arr)
|
penalty_points = self.checkShape(projected_x, projected_y, shape_arr)
|
||||||
if penalty_points is not None:
|
if penalty_points is not None:
|
||||||
return LocationSuggestion(x = projected_x, y = projected_y, penalty_points = penalty_points, priority = priority)
|
return LocationSuggestion(x = projected_x, y = projected_y, penalty_points = penalty_points, priority = priority)
|
||||||
|
@ -191,8 +206,12 @@ class Arrange:
|
||||||
|
|
||||||
# Set priority to low (= high number), so it won't get picked at trying out.
|
# Set priority to low (= high number), so it won't get picked at trying out.
|
||||||
prio_slice = self._priority[min_y:max_y, min_x:max_x]
|
prio_slice = self._priority[min_y:max_y, min_x:max_x]
|
||||||
prio_slice[numpy.where(shape_arr.arr[
|
prio_slice[new_occupied] = 999
|
||||||
min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 999
|
|
||||||
|
# If you want to see how the rasterized arranger build plate looks like, uncomment this code
|
||||||
|
# numpy.set_printoptions(linewidth=500, edgeitems=200)
|
||||||
|
# print(self._occupied.shape)
|
||||||
|
# print(self._occupied)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def isEmpty(self):
|
def isEmpty(self):
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
from UM.Job import Job
|
from UM.Job import Job
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
|
@ -17,6 +18,7 @@ from cura.Arranging.ShapeArray import ShapeArray
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
## Do arrangements on multiple build plates (aka builtiplexer)
|
||||||
class ArrangeArray:
|
class ArrangeArray:
|
||||||
def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]):
|
def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]):
|
||||||
self._x = x
|
self._x = x
|
||||||
|
@ -79,7 +81,11 @@ class ArrangeObjectsAllBuildPlatesJob(Job):
|
||||||
nodes_arr.sort(key=lambda item: item[0])
|
nodes_arr.sort(key=lambda item: item[0])
|
||||||
nodes_arr.reverse()
|
nodes_arr.reverse()
|
||||||
|
|
||||||
x, y = 200, 200
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
machine_width = global_container_stack.getProperty("machine_width", "value")
|
||||||
|
machine_depth = global_container_stack.getProperty("machine_depth", "value")
|
||||||
|
|
||||||
|
x, y = machine_width, machine_depth
|
||||||
|
|
||||||
arrange_array = ArrangeArray(x = x, y = y, fixed_nodes = [])
|
arrange_array = ArrangeArray(x = x, y = y, fixed_nodes = [])
|
||||||
arrange_array.add()
|
arrange_array.add()
|
||||||
|
@ -93,27 +99,18 @@ class ArrangeObjectsAllBuildPlatesJob(Job):
|
||||||
for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr):
|
for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr):
|
||||||
# For performance reasons, we assume that when a location does not fit,
|
# For performance reasons, we assume that when a location does not fit,
|
||||||
# it will also not fit for the next object (while what can be untrue).
|
# it will also not fit for the next object (while what can be untrue).
|
||||||
# We also skip possibilities by slicing through the possibilities (step = 10)
|
|
||||||
|
|
||||||
try_placement = True
|
try_placement = True
|
||||||
|
|
||||||
current_build_plate_number = 0 # always start with the first one
|
current_build_plate_number = 0 # always start with the first one
|
||||||
|
|
||||||
# # Only for first build plate
|
|
||||||
# if last_size == size and last_build_plate_number == current_build_plate_number:
|
|
||||||
# # This optimization works if many of the objects have the same size
|
|
||||||
# # Continue with same build plate number
|
|
||||||
# start_priority = last_priority
|
|
||||||
# else:
|
|
||||||
# start_priority = 0
|
|
||||||
|
|
||||||
while try_placement:
|
while try_placement:
|
||||||
# make sure that current_build_plate_number is not going crazy or you'll have a lot of arrange objects
|
# make sure that current_build_plate_number is not going crazy or you'll have a lot of arrange objects
|
||||||
while current_build_plate_number >= arrange_array.count():
|
while current_build_plate_number >= arrange_array.count():
|
||||||
arrange_array.add()
|
arrange_array.add()
|
||||||
arranger = arrange_array.get(current_build_plate_number)
|
arranger = arrange_array.get(current_build_plate_number)
|
||||||
|
|
||||||
best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority, step=10)
|
best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority)
|
||||||
x, y = best_spot.x, best_spot.y
|
x, y = best_spot.x, best_spot.y
|
||||||
node.removeDecorator(ZOffsetDecorator)
|
node.removeDecorator(ZOffsetDecorator)
|
||||||
if node.getBoundingBox():
|
if node.getBoundingBox():
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
from UM.Job import Job
|
from UM.Job import Job
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
|
@ -32,7 +33,11 @@ class ArrangeObjectsJob(Job):
|
||||||
progress = 0,
|
progress = 0,
|
||||||
title = i18n_catalog.i18nc("@info:title", "Finding Location"))
|
title = i18n_catalog.i18nc("@info:title", "Finding Location"))
|
||||||
status_message.show()
|
status_message.show()
|
||||||
arranger = Arrange.create(fixed_nodes = self._fixed_nodes)
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
machine_width = global_container_stack.getProperty("machine_width", "value")
|
||||||
|
machine_depth = global_container_stack.getProperty("machine_depth", "value")
|
||||||
|
|
||||||
|
arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = self._fixed_nodes)
|
||||||
|
|
||||||
# Collect nodes to be placed
|
# Collect nodes to be placed
|
||||||
nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr)
|
nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr)
|
||||||
|
@ -50,15 +55,15 @@ class ArrangeObjectsJob(Job):
|
||||||
last_size = None
|
last_size = None
|
||||||
grouped_operation = GroupedOperation()
|
grouped_operation = GroupedOperation()
|
||||||
found_solution_for_all = True
|
found_solution_for_all = True
|
||||||
|
not_fit_count = 0
|
||||||
for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr):
|
for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr):
|
||||||
# For performance reasons, we assume that when a location does not fit,
|
# For performance reasons, we assume that when a location does not fit,
|
||||||
# it will also not fit for the next object (while what can be untrue).
|
# it will also not fit for the next object (while what can be untrue).
|
||||||
# We also skip possibilities by slicing through the possibilities (step = 10)
|
|
||||||
if last_size == size: # This optimization works if many of the objects have the same size
|
if last_size == size: # This optimization works if many of the objects have the same size
|
||||||
start_priority = last_priority
|
start_priority = last_priority
|
||||||
else:
|
else:
|
||||||
start_priority = 0
|
start_priority = 0
|
||||||
best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority, step=10)
|
best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority)
|
||||||
x, y = best_spot.x, best_spot.y
|
x, y = best_spot.x, best_spot.y
|
||||||
node.removeDecorator(ZOffsetDecorator)
|
node.removeDecorator(ZOffsetDecorator)
|
||||||
if node.getBoundingBox():
|
if node.getBoundingBox():
|
||||||
|
@ -70,12 +75,12 @@ class ArrangeObjectsJob(Job):
|
||||||
last_priority = best_spot.priority
|
last_priority = best_spot.priority
|
||||||
|
|
||||||
arranger.place(x, y, hull_shape_arr) # take place before the next one
|
arranger.place(x, y, hull_shape_arr) # take place before the next one
|
||||||
|
|
||||||
grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))
|
grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))
|
||||||
else:
|
else:
|
||||||
Logger.log("d", "Arrange all: could not find spot!")
|
Logger.log("d", "Arrange all: could not find spot!")
|
||||||
found_solution_for_all = False
|
found_solution_for_all = False
|
||||||
grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, - idx * 20), set_position = True))
|
grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, -not_fit_count * 20), set_position = True))
|
||||||
|
not_fit_count += 1
|
||||||
|
|
||||||
status_message.setProgress((idx + 1) / len(nodes_arr) * 100)
|
status_message.setProgress((idx + 1) / len(nodes_arr) * 100)
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
|
|
|
@ -74,7 +74,7 @@ class ShapeArray:
|
||||||
# \param vertices
|
# \param vertices
|
||||||
@classmethod
|
@classmethod
|
||||||
def arrayFromPolygon(cls, shape, vertices):
|
def arrayFromPolygon(cls, shape, vertices):
|
||||||
base_array = numpy.zeros(shape, dtype=float) # Initialize your array of zeros
|
base_array = numpy.zeros(shape, dtype = numpy.int32) # Initialize your array of zeros
|
||||||
|
|
||||||
fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill
|
fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill
|
||||||
|
|
||||||
|
|
52
cura/AutoSave.py
Normal file
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.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
import math
|
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
import numpy
|
|
||||||
|
|
||||||
from PyQt5.QtCore import QTimer
|
|
||||||
|
|
||||||
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Scene.Platform import Platform
|
from UM.Scene.Platform import Platform
|
||||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||||
|
@ -20,14 +16,17 @@ from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||||
from UM.Math.Polygon import Polygon
|
from UM.Math.Polygon import Polygon
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
from UM.Signal import Signal
|
from UM.Signal import Signal
|
||||||
|
from PyQt5.QtCore import QTimer
|
||||||
from UM.View.RenderBatch import RenderBatch
|
from UM.View.RenderBatch import RenderBatch
|
||||||
from UM.View.GL.OpenGL import OpenGL
|
from UM.View.GL.OpenGL import OpenGL
|
||||||
|
|
||||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
import math
|
||||||
|
import copy
|
||||||
|
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
# Setting for clearance around the prime
|
# Setting for clearance around the prime
|
||||||
PRIME_CLEARANCE = 6.5
|
PRIME_CLEARANCE = 6.5
|
||||||
|
|
||||||
|
@ -63,6 +62,7 @@ class BuildVolume(SceneNode):
|
||||||
self._grid_shader = None
|
self._grid_shader = None
|
||||||
|
|
||||||
self._disallowed_areas = []
|
self._disallowed_areas = []
|
||||||
|
self._disallowed_areas_no_brim = []
|
||||||
self._disallowed_area_mesh = None
|
self._disallowed_area_mesh = None
|
||||||
|
|
||||||
self._error_areas = []
|
self._error_areas = []
|
||||||
|
@ -173,6 +173,9 @@ class BuildVolume(SceneNode):
|
||||||
def getDisallowedAreas(self) -> List[Polygon]:
|
def getDisallowedAreas(self) -> List[Polygon]:
|
||||||
return self._disallowed_areas
|
return self._disallowed_areas
|
||||||
|
|
||||||
|
def getDisallowedAreasNoBrim(self) -> List[Polygon]:
|
||||||
|
return self._disallowed_areas_no_brim
|
||||||
|
|
||||||
def setDisallowedAreas(self, areas: List[Polygon]):
|
def setDisallowedAreas(self, areas: List[Polygon]):
|
||||||
self._disallowed_areas = areas
|
self._disallowed_areas = areas
|
||||||
|
|
||||||
|
@ -457,7 +460,7 @@ class BuildVolume(SceneNode):
|
||||||
minimum = Vector(min_w, min_h - 1.0, min_d),
|
minimum = Vector(min_w, min_h - 1.0, min_d),
|
||||||
maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d))
|
maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d))
|
||||||
|
|
||||||
bed_adhesion_size = self._getEdgeDisallowedSize()
|
bed_adhesion_size = self.getEdgeDisallowedSize()
|
||||||
|
|
||||||
# As this works better for UM machines, we only add the disallowed_area_size for the z direction.
|
# As this works better for UM machines, we only add the disallowed_area_size for the z direction.
|
||||||
# This is probably wrong in all other cases. TODO!
|
# This is probably wrong in all other cases. TODO!
|
||||||
|
@ -649,7 +652,7 @@ class BuildVolume(SceneNode):
|
||||||
|
|
||||||
extruder_manager = ExtruderManager.getInstance()
|
extruder_manager = ExtruderManager.getInstance()
|
||||||
used_extruders = extruder_manager.getUsedExtruderStacks()
|
used_extruders = extruder_manager.getUsedExtruderStacks()
|
||||||
disallowed_border_size = self._getEdgeDisallowedSize()
|
disallowed_border_size = self.getEdgeDisallowedSize()
|
||||||
|
|
||||||
if not used_extruders:
|
if not used_extruders:
|
||||||
# If no extruder is used, assume that the active extruder is used (else nothing is drawn)
|
# If no extruder is used, assume that the active extruder is used (else nothing is drawn)
|
||||||
|
@ -660,7 +663,8 @@ class BuildVolume(SceneNode):
|
||||||
|
|
||||||
result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) #Normal machine disallowed areas can always be added.
|
result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) #Normal machine disallowed areas can always be added.
|
||||||
prime_areas = self._computeDisallowedAreasPrimeBlob(disallowed_border_size, used_extruders)
|
prime_areas = self._computeDisallowedAreasPrimeBlob(disallowed_border_size, used_extruders)
|
||||||
prime_disallowed_areas = self._computeDisallowedAreasStatic(0, used_extruders) #Where the priming is not allowed to happen. This is not added to the result, just for collision checking.
|
result_areas_no_brim = self._computeDisallowedAreasStatic(0, used_extruders) #Where the priming is not allowed to happen. This is not added to the result, just for collision checking.
|
||||||
|
prime_disallowed_areas = copy.deepcopy(result_areas_no_brim)
|
||||||
|
|
||||||
#Check if prime positions intersect with disallowed areas.
|
#Check if prime positions intersect with disallowed areas.
|
||||||
for extruder in used_extruders:
|
for extruder in used_extruders:
|
||||||
|
@ -689,12 +693,15 @@ class BuildVolume(SceneNode):
|
||||||
break
|
break
|
||||||
|
|
||||||
result_areas[extruder_id].extend(prime_areas[extruder_id])
|
result_areas[extruder_id].extend(prime_areas[extruder_id])
|
||||||
|
result_areas_no_brim[extruder_id].extend(prime_areas[extruder_id])
|
||||||
|
|
||||||
nozzle_disallowed_areas = extruder.getProperty("nozzle_disallowed_areas", "value")
|
nozzle_disallowed_areas = extruder.getProperty("nozzle_disallowed_areas", "value")
|
||||||
for area in nozzle_disallowed_areas:
|
for area in nozzle_disallowed_areas:
|
||||||
polygon = Polygon(numpy.array(area, numpy.float32))
|
polygon = Polygon(numpy.array(area, numpy.float32))
|
||||||
polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size))
|
polygon_disallowed_border = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size))
|
||||||
result_areas[extruder_id].append(polygon) #Don't perform the offset on these.
|
result_areas[extruder_id].append(polygon_disallowed_border) #Don't perform the offset on these.
|
||||||
|
#polygon_minimal_border = polygon.getMinkowskiHull(5)
|
||||||
|
result_areas_no_brim[extruder_id].append(polygon) # no brim
|
||||||
|
|
||||||
# Add prime tower location as disallowed area.
|
# Add prime tower location as disallowed area.
|
||||||
if len(used_extruders) > 1: #No prime tower in single-extrusion.
|
if len(used_extruders) > 1: #No prime tower in single-extrusion.
|
||||||
|
@ -710,6 +717,7 @@ class BuildVolume(SceneNode):
|
||||||
break
|
break
|
||||||
if not prime_tower_collision:
|
if not prime_tower_collision:
|
||||||
result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
|
result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
|
||||||
|
result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
|
||||||
else:
|
else:
|
||||||
self._error_areas.extend(prime_tower_areas[extruder_id])
|
self._error_areas.extend(prime_tower_areas[extruder_id])
|
||||||
|
|
||||||
|
@ -718,6 +726,9 @@ class BuildVolume(SceneNode):
|
||||||
self._disallowed_areas = []
|
self._disallowed_areas = []
|
||||||
for extruder_id in result_areas:
|
for extruder_id in result_areas:
|
||||||
self._disallowed_areas.extend(result_areas[extruder_id])
|
self._disallowed_areas.extend(result_areas[extruder_id])
|
||||||
|
self._disallowed_areas_no_brim = []
|
||||||
|
for extruder_id in result_areas_no_brim:
|
||||||
|
self._disallowed_areas_no_brim.extend(result_areas_no_brim[extruder_id])
|
||||||
|
|
||||||
## Computes the disallowed areas for objects that are printed with print
|
## Computes the disallowed areas for objects that are printed with print
|
||||||
# features.
|
# features.
|
||||||
|
@ -951,12 +962,12 @@ class BuildVolume(SceneNode):
|
||||||
all_values[i] = 0
|
all_values[i] = 0
|
||||||
return all_values
|
return all_values
|
||||||
|
|
||||||
## Convenience function to calculate the disallowed radius around the edge.
|
## Calculate the disallowed radius around the edge.
|
||||||
#
|
#
|
||||||
# This disallowed radius is to allow for space around the models that is
|
# This disallowed radius is to allow for space around the models that is
|
||||||
# not part of the collision radius, such as bed adhesion (skirt/brim/raft)
|
# not part of the collision radius, such as bed adhesion (skirt/brim/raft)
|
||||||
# and travel avoid distance.
|
# and travel avoid distance.
|
||||||
def _getEdgeDisallowedSize(self):
|
def getEdgeDisallowedSize(self):
|
||||||
if not self._global_container_stack or not self._global_container_stack.extruders:
|
if not self._global_container_stack or not self._global_container_stack.extruders:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
@ -1037,6 +1048,6 @@ class BuildVolume(SceneNode):
|
||||||
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"]
|
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"]
|
||||||
_tower_settings = ["prime_tower_enable", "prime_tower_circular", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"]
|
_tower_settings = ["prime_tower_enable", "prime_tower_circular", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"]
|
||||||
_ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
|
_ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
|
||||||
_distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts"]
|
_distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports"]
|
||||||
_extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
|
_extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
|
||||||
_limit_to_extruder_settings = ["wall_extruder_nr", "wall_0_extruder_nr", "wall_x_extruder_nr", "top_bottom_extruder_nr", "infill_extruder_nr", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "adhesion_extruder_nr"]
|
_limit_to_extruder_settings = ["wall_extruder_nr", "wall_0_extruder_nr", "wall_x_extruder_nr", "top_bottom_extruder_nr", "infill_extruder_nr", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "adhesion_extruder_nr"]
|
||||||
|
|
|
@ -72,7 +72,8 @@ class CuraActions(QObject):
|
||||||
# \param count The number of times to multiply the selection.
|
# \param count The number of times to multiply the selection.
|
||||||
@pyqtSlot(int)
|
@pyqtSlot(int)
|
||||||
def multiplySelection(self, count: int) -> None:
|
def multiplySelection(self, count: int) -> None:
|
||||||
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = 8)
|
min_offset = Application.getInstance().getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
|
||||||
|
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = max(min_offset, 8))
|
||||||
job.start()
|
job.start()
|
||||||
|
|
||||||
## Delete all selected objects.
|
## Delete all selected objects.
|
||||||
|
|
|
@ -85,6 +85,7 @@ from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
|
||||||
from cura.Machines.VariantManager import VariantManager
|
from cura.Machines.VariantManager import VariantManager
|
||||||
|
|
||||||
from .SingleInstance import SingleInstance
|
from .SingleInstance import SingleInstance
|
||||||
|
from .AutoSave import AutoSave
|
||||||
from . import PlatformPhysics
|
from . import PlatformPhysics
|
||||||
from . import BuildVolume
|
from . import BuildVolume
|
||||||
from . import CameraAnimation
|
from . import CameraAnimation
|
||||||
|
@ -154,9 +155,6 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
self._boot_loading_time = time.time()
|
self._boot_loading_time = time.time()
|
||||||
|
|
||||||
self._currently_loading_files = []
|
|
||||||
self._non_sliceable_extensions = []
|
|
||||||
|
|
||||||
# Variables set from CLI
|
# Variables set from CLI
|
||||||
self._files_to_open = []
|
self._files_to_open = []
|
||||||
self._use_single_instance = False
|
self._use_single_instance = False
|
||||||
|
@ -223,6 +221,10 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
self._need_to_show_user_agreement = True
|
self._need_to_show_user_agreement = True
|
||||||
|
|
||||||
|
# Backups
|
||||||
|
self._auto_save = None
|
||||||
|
self._save_data_enabled = True
|
||||||
|
|
||||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||||
self._container_registry_class = CuraContainerRegistry
|
self._container_registry_class = CuraContainerRegistry
|
||||||
|
|
||||||
|
@ -469,6 +471,7 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
preferences.addPreference("cura/categories_expanded", "")
|
preferences.addPreference("cura/categories_expanded", "")
|
||||||
preferences.addPreference("cura/jobname_prefix", True)
|
preferences.addPreference("cura/jobname_prefix", True)
|
||||||
|
preferences.addPreference("cura/select_models_on_load", False)
|
||||||
preferences.addPreference("view/center_on_select", False)
|
preferences.addPreference("view/center_on_select", False)
|
||||||
preferences.addPreference("mesh/scale_to_fit", False)
|
preferences.addPreference("mesh/scale_to_fit", False)
|
||||||
preferences.addPreference("mesh/scale_tiny_meshes", True)
|
preferences.addPreference("mesh/scale_tiny_meshes", True)
|
||||||
|
@ -585,14 +588,17 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
showPrintMonitor = pyqtSignal(bool, arguments = ["show"])
|
showPrintMonitor = pyqtSignal(bool, arguments = ["show"])
|
||||||
|
|
||||||
## Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
|
def setSaveDataEnabled(self, enabled: bool) -> None:
|
||||||
#
|
self._save_data_enabled = enabled
|
||||||
# Note that the AutoSave plugin also calls this method.
|
|
||||||
def saveSettings(self):
|
|
||||||
if not self.started: # Do not do saving during application start
|
|
||||||
return
|
|
||||||
|
|
||||||
|
# Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
|
||||||
|
def saveSettings(self):
|
||||||
|
if not self.started or not self._save_data_enabled:
|
||||||
|
# Do not do saving during application start or when data should not be safed on quit.
|
||||||
|
return
|
||||||
ContainerRegistry.getInstance().saveDirtyContainers()
|
ContainerRegistry.getInstance().saveDirtyContainers()
|
||||||
|
Preferences.getInstance().writeToFile(Resources.getStoragePath(Resources.Preferences,
|
||||||
|
self._application_name + ".cfg"))
|
||||||
|
|
||||||
def saveStack(self, stack):
|
def saveStack(self, stack):
|
||||||
ContainerRegistry.getInstance().saveContainer(stack)
|
ContainerRegistry.getInstance().saveContainer(stack)
|
||||||
|
@ -695,6 +701,9 @@ class CuraApplication(QtApplication):
|
||||||
self._post_start_timer.timeout.connect(self._onPostStart)
|
self._post_start_timer.timeout.connect(self._onPostStart)
|
||||||
self._post_start_timer.start()
|
self._post_start_timer.start()
|
||||||
|
|
||||||
|
self._auto_save = AutoSave(self)
|
||||||
|
self._auto_save.initialize()
|
||||||
|
|
||||||
self.exec_()
|
self.exec_()
|
||||||
|
|
||||||
def __setUpSingleInstanceServer(self):
|
def __setUpSingleInstanceServer(self):
|
||||||
|
@ -844,6 +853,9 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
return super().event(event)
|
return super().event(event)
|
||||||
|
|
||||||
|
def getAutoSave(self):
|
||||||
|
return self._auto_save
|
||||||
|
|
||||||
## Get print information (duration / material used)
|
## Get print information (duration / material used)
|
||||||
def getPrintInformation(self):
|
def getPrintInformation(self):
|
||||||
return self._print_information
|
return self._print_information
|
||||||
|
@ -1228,34 +1240,12 @@ class CuraApplication(QtApplication):
|
||||||
nodes.append(node)
|
nodes.append(node)
|
||||||
self.arrange(nodes, fixed_nodes = [])
|
self.arrange(nodes, fixed_nodes = [])
|
||||||
|
|
||||||
## Arrange Selection
|
|
||||||
@pyqtSlot()
|
|
||||||
def arrangeSelection(self):
|
|
||||||
nodes = Selection.getAllSelectedObjects()
|
|
||||||
|
|
||||||
# What nodes are on the build plate and are not being moved
|
|
||||||
fixed_nodes = []
|
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
|
||||||
if not isinstance(node, SceneNode):
|
|
||||||
continue
|
|
||||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
|
||||||
continue # Node that doesnt have a mesh and is not a group.
|
|
||||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
|
||||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
|
||||||
if not node.isSelectable():
|
|
||||||
continue # i.e. node with layer data
|
|
||||||
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
|
||||||
continue # i.e. node with layer data
|
|
||||||
if node in nodes: # exclude selected node from fixed_nodes
|
|
||||||
continue
|
|
||||||
fixed_nodes.append(node)
|
|
||||||
self.arrange(nodes, fixed_nodes)
|
|
||||||
|
|
||||||
## Arrange a set of nodes given a set of fixed nodes
|
## Arrange a set of nodes given a set of fixed nodes
|
||||||
# \param nodes nodes that we have to place
|
# \param nodes nodes that we have to place
|
||||||
# \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes
|
# \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes
|
||||||
def arrange(self, nodes, fixed_nodes):
|
def arrange(self, nodes, fixed_nodes):
|
||||||
job = ArrangeObjectsJob(nodes, fixed_nodes)
|
min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
|
||||||
|
job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8))
|
||||||
job.start()
|
job.start()
|
||||||
|
|
||||||
## Reload all mesh data on the screen from file.
|
## Reload all mesh data on the screen from file.
|
||||||
|
@ -1539,6 +1529,9 @@ class CuraApplication(QtApplication):
|
||||||
self.callLater(self.openProjectFile.emit, file)
|
self.callLater(self.openProjectFile.emit, file)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if Preferences.getInstance().getValue("cura/select_models_on_load"):
|
||||||
|
Selection.clear()
|
||||||
|
|
||||||
f = file.toLocalFile()
|
f = file.toLocalFile()
|
||||||
extension = os.path.splitext(f)[1]
|
extension = os.path.splitext(f)[1]
|
||||||
filename = os.path.basename(f)
|
filename = os.path.basename(f)
|
||||||
|
@ -1585,11 +1578,16 @@ class CuraApplication(QtApplication):
|
||||||
for node_ in DepthFirstIterator(root):
|
for node_ in DepthFirstIterator(root):
|
||||||
if node_.callDecoration("isSliceable") and node_.callDecoration("getBuildPlateNumber") == target_build_plate:
|
if node_.callDecoration("isSliceable") and node_.callDecoration("getBuildPlateNumber") == target_build_plate:
|
||||||
fixed_nodes.append(node_)
|
fixed_nodes.append(node_)
|
||||||
arranger = Arrange.create(fixed_nodes = fixed_nodes)
|
global_container_stack = self.getGlobalContainerStack()
|
||||||
|
machine_width = global_container_stack.getProperty("machine_width", "value")
|
||||||
|
machine_depth = global_container_stack.getProperty("machine_depth", "value")
|
||||||
|
arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = fixed_nodes)
|
||||||
min_offset = 8
|
min_offset = 8
|
||||||
default_extruder_position = self.getMachineManager().defaultExtruderPosition
|
default_extruder_position = self.getMachineManager().defaultExtruderPosition
|
||||||
default_extruder_id = self._global_container_stack.extruders[default_extruder_position].getId()
|
default_extruder_id = self._global_container_stack.extruders[default_extruder_position].getId()
|
||||||
|
|
||||||
|
select_models_on_load = Preferences.getInstance().getValue("cura/select_models_on_load")
|
||||||
|
|
||||||
for original_node in nodes:
|
for original_node in nodes:
|
||||||
|
|
||||||
# Create a CuraSceneNode just if the original node is not that type
|
# Create a CuraSceneNode just if the original node is not that type
|
||||||
|
@ -1603,7 +1601,6 @@ class CuraApplication(QtApplication):
|
||||||
if(original_node.getScale() != Vector(1.0, 1.0, 1.0)):
|
if(original_node.getScale() != Vector(1.0, 1.0, 1.0)):
|
||||||
node.scale(original_node.getScale())
|
node.scale(original_node.getScale())
|
||||||
|
|
||||||
|
|
||||||
node.setSelectable(True)
|
node.setSelectable(True)
|
||||||
node.setName(os.path.basename(filename))
|
node.setName(os.path.basename(filename))
|
||||||
self.getBuildVolume().checkBoundsAndUpdate(node)
|
self.getBuildVolume().checkBoundsAndUpdate(node)
|
||||||
|
@ -1663,6 +1660,9 @@ class CuraApplication(QtApplication):
|
||||||
node.callDecoration("setActiveExtruder", default_extruder_id)
|
node.callDecoration("setActiveExtruder", default_extruder_id)
|
||||||
scene.sceneChanged.emit(node)
|
scene.sceneChanged.emit(node)
|
||||||
|
|
||||||
|
if select_models_on_load:
|
||||||
|
Selection.add(node)
|
||||||
|
|
||||||
self.fileCompleted.emit(filename)
|
self.fileCompleted.emit(filename)
|
||||||
|
|
||||||
def addNonSliceableExtension(self, extension):
|
def addNonSliceableExtension(self, extension):
|
||||||
|
|
|
@ -30,11 +30,18 @@ class MultiplyObjectsJob(Job):
|
||||||
total_progress = len(self._objects) * self._count
|
total_progress = len(self._objects) * self._count
|
||||||
current_progress = 0
|
current_progress = 0
|
||||||
|
|
||||||
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
machine_width = global_container_stack.getProperty("machine_width", "value")
|
||||||
|
machine_depth = global_container_stack.getProperty("machine_depth", "value")
|
||||||
|
|
||||||
root = scene.getRoot()
|
root = scene.getRoot()
|
||||||
arranger = Arrange.create(scene_root=root)
|
scale = 0.5
|
||||||
|
arranger = Arrange.create(x = machine_width, y = machine_depth, scene_root = root, scale = scale)
|
||||||
processed_nodes = []
|
processed_nodes = []
|
||||||
nodes = []
|
nodes = []
|
||||||
|
|
||||||
|
not_fit_count = 0
|
||||||
|
|
||||||
for node in self._objects:
|
for node in self._objects:
|
||||||
# If object is part of a group, multiply group
|
# If object is part of a group, multiply group
|
||||||
current_node = node
|
current_node = node
|
||||||
|
@ -46,12 +53,13 @@ class MultiplyObjectsJob(Job):
|
||||||
processed_nodes.append(current_node)
|
processed_nodes.append(current_node)
|
||||||
|
|
||||||
node_too_big = False
|
node_too_big = False
|
||||||
if node.getBoundingBox().width < 300 or node.getBoundingBox().depth < 300:
|
if node.getBoundingBox().width < machine_width or node.getBoundingBox().depth < machine_depth:
|
||||||
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset=self._min_offset)
|
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset = self._min_offset, scale = scale)
|
||||||
else:
|
else:
|
||||||
node_too_big = True
|
node_too_big = True
|
||||||
|
|
||||||
found_solution_for_all = True
|
found_solution_for_all = True
|
||||||
|
arranger.resetLastPriority()
|
||||||
for i in range(self._count):
|
for i in range(self._count):
|
||||||
# We do place the nodes one by one, as we want to yield in between.
|
# We do place the nodes one by one, as we want to yield in between.
|
||||||
if not node_too_big:
|
if not node_too_big:
|
||||||
|
@ -59,8 +67,9 @@ class MultiplyObjectsJob(Job):
|
||||||
if node_too_big or not solution_found:
|
if node_too_big or not solution_found:
|
||||||
found_solution_for_all = False
|
found_solution_for_all = False
|
||||||
new_location = new_node.getPosition()
|
new_location = new_node.getPosition()
|
||||||
new_location = new_location.set(z = 100 - i * 20)
|
new_location = new_location.set(z = - not_fit_count * 20)
|
||||||
new_node.setPosition(new_location)
|
new_node.setPosition(new_location)
|
||||||
|
not_fit_count += 1
|
||||||
|
|
||||||
# Same build plate
|
# Same build plate
|
||||||
build_plate_number = current_node.callDecoration("getBuildPlateNumber")
|
build_plate_number = current_node.callDecoration("getBuildPlateNumber")
|
||||||
|
|
|
@ -79,10 +79,10 @@ class PreviewPass(RenderPass):
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
|
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
|
||||||
per_mesh_stack = node.callDecoration("getStack")
|
per_mesh_stack = node.callDecoration("getStack")
|
||||||
if node.callDecoration("isNonPrintingMesh"):
|
if node.callDecoration("isNonThumbnailVisibleMesh"):
|
||||||
# Non printing mesh
|
# Non printing mesh
|
||||||
continue
|
continue
|
||||||
elif per_mesh_stack is not None and per_mesh_stack.getProperty("support_mesh", "value") == True:
|
elif per_mesh_stack is not None and per_mesh_stack.getProperty("support_mesh", "value"):
|
||||||
# Support mesh
|
# Support mesh
|
||||||
uniforms = {}
|
uniforms = {}
|
||||||
shade_factor = 0.6
|
shade_factor = 0.6
|
||||||
|
@ -112,4 +112,3 @@ class PreviewPass(RenderPass):
|
||||||
batch_support_mesh.render(render_camera)
|
batch_support_mesh.render(render_camera)
|
||||||
|
|
||||||
self.release()
|
self.release()
|
||||||
|
|
||||||
|
|
|
@ -279,9 +279,12 @@ class PrintInformation(QObject):
|
||||||
for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
|
for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
|
||||||
self._calculateInformation(build_plate_number)
|
self._calculateInformation(build_plate_number)
|
||||||
|
|
||||||
|
# Manual override of job name should also set the base name so that when the printer prefix is updated, it the
|
||||||
|
# prefix can be added to the manually added name, not the old base name
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def setJobName(self, name):
|
def setJobName(self, name):
|
||||||
self._job_name = name
|
self._job_name = name
|
||||||
|
self._base_name = name.replace(self._abbr_machine + "_", "")
|
||||||
self.jobNameChanged.emit()
|
self.jobNameChanged.emit()
|
||||||
|
|
||||||
jobNameChanged = pyqtSignal()
|
jobNameChanged = pyqtSignal()
|
||||||
|
|
|
@ -30,6 +30,7 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||||
# Note that Support Mesh is not in here because it actually generates
|
# Note that Support Mesh is not in here because it actually generates
|
||||||
# g-code in the volume of the mesh.
|
# g-code in the volume of the mesh.
|
||||||
_non_printing_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"}
|
_non_printing_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"}
|
||||||
|
_non_thumbnail_visible_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh", "support_mesh"}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
@ -41,6 +42,7 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||||
self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0).getId()
|
self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0).getId()
|
||||||
|
|
||||||
self._is_non_printing_mesh = False
|
self._is_non_printing_mesh = False
|
||||||
|
self._is_non_thumbnail_visible_mesh = False
|
||||||
|
|
||||||
self._stack.propertyChanged.connect(self._onSettingChanged)
|
self._stack.propertyChanged.connect(self._onSettingChanged)
|
||||||
|
|
||||||
|
@ -72,6 +74,7 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||||
# use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh"
|
# use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh"
|
||||||
# has not been updated yet.
|
# has not been updated yet.
|
||||||
deep_copy._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
|
deep_copy._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
|
||||||
|
deep_copy._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh()
|
||||||
|
|
||||||
return deep_copy
|
return deep_copy
|
||||||
|
|
||||||
|
@ -102,10 +105,17 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||||
def evaluateIsNonPrintingMesh(self):
|
def evaluateIsNonPrintingMesh(self):
|
||||||
return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings)
|
return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings)
|
||||||
|
|
||||||
|
def isNonThumbnailVisibleMesh(self):
|
||||||
|
return self._is_non_thumbnail_visible_mesh
|
||||||
|
|
||||||
|
def evaluateIsNonThumbnailVisibleMesh(self):
|
||||||
|
return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_thumbnail_visible_settings)
|
||||||
|
|
||||||
def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function
|
def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function
|
||||||
if property_name == "value":
|
if property_name == "value":
|
||||||
# Trigger slice/need slicing if the value has changed.
|
# Trigger slice/need slicing if the value has changed.
|
||||||
self._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
|
self._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
|
||||||
|
self._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh()
|
||||||
|
|
||||||
Application.getInstance().getBackend().needsSlicing()
|
Application.getInstance().getBackend().needsSlicing()
|
||||||
Application.getInstance().getBackend().tickle()
|
Application.getInstance().getBackend().tickle()
|
||||||
|
|
|
@ -48,7 +48,7 @@ class Snapshot:
|
||||||
# determine zoom and look at
|
# determine zoom and look at
|
||||||
bbox = None
|
bbox = None
|
||||||
for node in DepthFirstIterator(root):
|
for node in DepthFirstIterator(root):
|
||||||
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration("isNonPrintingMesh"):
|
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration("isNonThumbnailVisibleMesh"):
|
||||||
if bbox is None:
|
if bbox is None:
|
||||||
bbox = node.getBoundingBox()
|
bbox = node.getBoundingBox()
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from PyQt5.QtCore import pyqtProperty, QUrl, QObject
|
from PyQt5.QtCore import pyqtProperty, QUrl
|
||||||
|
|
||||||
from UM.Stage import Stage
|
from UM.Stage import Stage
|
||||||
|
|
||||||
|
|
||||||
class CuraStage(Stage):
|
class CuraStage(Stage):
|
||||||
|
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
|
|
|
@ -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")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
def getMetaData():
|
def getMetaData():
|
||||||
file_extension = "gz" if Platform.isOSX() else "gcode.gz"
|
file_extension = "gcode.gz"
|
||||||
return {
|
return {
|
||||||
"mesh_writer": {
|
"mesh_writer": {
|
||||||
"output": [{
|
"output": [{
|
||||||
|
|
|
@ -69,10 +69,11 @@ class MonitorStage(CuraStage):
|
||||||
self._updateSidebar()
|
self._updateSidebar()
|
||||||
|
|
||||||
def _updateMainOverlay(self):
|
def _updateMainOverlay(self):
|
||||||
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"), "MonitorMainView.qml")
|
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"),
|
||||||
|
"MonitorMainView.qml")
|
||||||
self.addDisplayComponent("main", main_component_path)
|
self.addDisplayComponent("main", main_component_path)
|
||||||
|
|
||||||
def _updateSidebar(self):
|
def _updateSidebar(self):
|
||||||
# TODO: currently the sidebar component for prepare and monitor stages is the same, this will change with the printer output device refactor!
|
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles),
|
||||||
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "Sidebar.qml")
|
"MonitorSidebar.qml")
|
||||||
self.addDisplayComponent("sidebar", sidebar_component_path)
|
self.addDisplayComponent("sidebar", sidebar_component_path)
|
||||||
|
|
|
@ -117,12 +117,21 @@ class PauseAtHeight(Script):
|
||||||
}
|
}
|
||||||
}"""
|
}"""
|
||||||
|
|
||||||
|
def getNextXY(self, layer: str):
|
||||||
|
"""
|
||||||
|
Get the X and Y values for a layer (will be used to get X and Y of
|
||||||
|
the layer after the pause
|
||||||
|
"""
|
||||||
|
lines = layer.split("\n")
|
||||||
|
for line in lines:
|
||||||
|
if self.getValue(line, "X") is not None and self.getValue(line, "Y") is not None:
|
||||||
|
x = self.getValue(line, "X")
|
||||||
|
y = self.getValue(line, "Y")
|
||||||
|
return x, y
|
||||||
|
return 0, 0
|
||||||
|
|
||||||
def execute(self, data: list):
|
def execute(self, data: list):
|
||||||
|
|
||||||
"""data is a list. Each index contains a layer"""
|
"""data is a list. Each index contains a layer"""
|
||||||
|
|
||||||
x = 0.
|
|
||||||
y = 0.
|
|
||||||
pause_at = self.getSettingValueByKey("pause_at")
|
pause_at = self.getSettingValueByKey("pause_at")
|
||||||
pause_height = self.getSettingValueByKey("pause_height")
|
pause_height = self.getSettingValueByKey("pause_height")
|
||||||
pause_layer = self.getSettingValueByKey("pause_layer")
|
pause_layer = self.getSettingValueByKey("pause_layer")
|
||||||
|
@ -138,73 +147,94 @@ class PauseAtHeight(Script):
|
||||||
resume_temperature = self.getSettingValueByKey("resume_temperature")
|
resume_temperature = self.getSettingValueByKey("resume_temperature")
|
||||||
|
|
||||||
# T = ExtruderManager.getInstance().getActiveExtruderStack().getProperty("material_print_temperature", "value")
|
# T = ExtruderManager.getInstance().getActiveExtruderStack().getProperty("material_print_temperature", "value")
|
||||||
# with open("out.txt", "w") as f:
|
|
||||||
# f.write(T)
|
|
||||||
|
|
||||||
# use offset to calculate the current height: <current_height> = <current_z> - <layer_0_z>
|
# use offset to calculate the current height: <current_height> = <current_z> - <layer_0_z>
|
||||||
layer_0_z = 0.
|
layer_0_z = 0.
|
||||||
current_z = 0
|
current_z = 0
|
||||||
got_first_g_cmd_on_layer_0 = False
|
got_first_g_cmd_on_layer_0 = False
|
||||||
|
|
||||||
|
nbr_negative_layers = 0
|
||||||
|
|
||||||
for index, layer in enumerate(data):
|
for index, layer in enumerate(data):
|
||||||
lines = layer.split("\n")
|
lines = layer.split("\n")
|
||||||
|
|
||||||
|
# Scroll each line of instruction for each layer in the G-code
|
||||||
for line in lines:
|
for line in lines:
|
||||||
|
# Fist positive layer reached
|
||||||
if ";LAYER:0" in line:
|
if ";LAYER:0" in line:
|
||||||
layers_started = True
|
layers_started = True
|
||||||
|
# Count nbr of negative layers (raft)
|
||||||
|
elif ";LAYER:-" in line:
|
||||||
|
nbr_negative_layers += 1
|
||||||
if not layers_started:
|
if not layers_started:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# If a Z instruction is in the line, read the current Z
|
||||||
if self.getValue(line, "Z") is not None:
|
if self.getValue(line, "Z") is not None:
|
||||||
current_z = self.getValue(line, "Z")
|
current_z = self.getValue(line, "Z")
|
||||||
|
|
||||||
if pause_at == "height":
|
if pause_at == "height":
|
||||||
|
# Ignore if the line is not G1 or G0
|
||||||
if self.getValue(line, "G") != 1 and self.getValue(line, "G") != 0:
|
if self.getValue(line, "G") != 1 and self.getValue(line, "G") != 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# This block is executed once, the first time there is a G
|
||||||
|
# command, to get the z offset (z for first positive layer)
|
||||||
if not got_first_g_cmd_on_layer_0:
|
if not got_first_g_cmd_on_layer_0:
|
||||||
layer_0_z = current_z
|
layer_0_z = current_z
|
||||||
got_first_g_cmd_on_layer_0 = True
|
got_first_g_cmd_on_layer_0 = True
|
||||||
|
|
||||||
x = self.getValue(line, "X", x)
|
|
||||||
y = self.getValue(line, "Y", y)
|
|
||||||
|
|
||||||
current_height = current_z - layer_0_z
|
current_height = current_z - layer_0_z
|
||||||
|
|
||||||
if current_height < pause_height:
|
if current_height < pause_height:
|
||||||
break #Try the next layer.
|
break # Try the next layer.
|
||||||
else: #Pause at layer.
|
|
||||||
|
# Pause at layer
|
||||||
|
else:
|
||||||
if not line.startswith(";LAYER:"):
|
if not line.startswith(";LAYER:"):
|
||||||
continue
|
continue
|
||||||
current_layer = line[len(";LAYER:"):]
|
current_layer = line[len(";LAYER:"):]
|
||||||
try:
|
try:
|
||||||
current_layer = int(current_layer)
|
current_layer = int(current_layer)
|
||||||
except ValueError: #Couldn't cast to int. Something is wrong with this g-code data.
|
|
||||||
continue
|
|
||||||
if current_layer < pause_layer:
|
|
||||||
break #Try the next layer.
|
|
||||||
|
|
||||||
prevLayer = data[index - 1]
|
# Couldn't cast to int. Something is wrong with this
|
||||||
prevLines = prevLayer.split("\n")
|
# g-code data
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if current_layer < pause_layer - nbr_negative_layers:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get X and Y from the next layer (better position for
|
||||||
|
# the nozzle)
|
||||||
|
next_layer = data[index + 1]
|
||||||
|
x, y = self.getNextXY(next_layer)
|
||||||
|
|
||||||
|
prev_layer = data[index - 1]
|
||||||
|
prev_lines = prev_layer.split("\n")
|
||||||
current_e = 0.
|
current_e = 0.
|
||||||
|
|
||||||
# Access last layer, browse it backwards to find
|
# Access last layer, browse it backwards to find
|
||||||
# last extruder absolute position
|
# last extruder absolute position
|
||||||
for prevLine in reversed(prevLines):
|
for prevLine in reversed(prev_lines):
|
||||||
current_e = self.getValue(prevLine, "E", -1)
|
current_e = self.getValue(prevLine, "E", -1)
|
||||||
if current_e >= 0:
|
if current_e >= 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
# include a number of previous layers
|
# include a number of previous layers
|
||||||
for i in range(1, redo_layers + 1):
|
for i in range(1, redo_layers + 1):
|
||||||
prevLayer = data[index - i]
|
prev_layer = data[index - i]
|
||||||
layer = prevLayer + layer
|
layer = prev_layer + layer
|
||||||
|
|
||||||
# Get extruder's absolute position at the
|
# Get extruder's absolute position at the
|
||||||
# begining of the first layer redone
|
# beginning of the first layer redone
|
||||||
# see https://github.com/nallath/PostProcessingPlugin/issues/55
|
# see https://github.com/nallath/PostProcessingPlugin/issues/55
|
||||||
if i == redo_layers:
|
if i == redo_layers:
|
||||||
prevLines = prevLayer.split("\n")
|
# Get X and Y from the next layer (better position for
|
||||||
for line in prevLines:
|
# the nozzle)
|
||||||
|
x, y = self.getNextXY(layer)
|
||||||
|
prev_lines = prev_layer.split("\n")
|
||||||
|
for line in prev_lines:
|
||||||
new_e = self.getValue(line, 'E', current_e)
|
new_e = self.getValue(line, 'E', current_e)
|
||||||
|
|
||||||
if new_e != current_e:
|
if new_e != current_e:
|
||||||
current_e = new_e
|
current_e = new_e
|
||||||
break
|
break
|
||||||
|
@ -213,61 +243,63 @@ class PauseAtHeight(Script):
|
||||||
prepend_gcode += ";added code by post processing\n"
|
prepend_gcode += ";added code by post processing\n"
|
||||||
prepend_gcode += ";script: PauseAtHeight.py\n"
|
prepend_gcode += ";script: PauseAtHeight.py\n"
|
||||||
if pause_at == "height":
|
if pause_at == "height":
|
||||||
prepend_gcode += ";current z: {z}\n".format(z = current_z)
|
prepend_gcode += ";current z: {z}\n".format(z=current_z)
|
||||||
prepend_gcode += ";current height: {height}\n".format(height = current_height)
|
prepend_gcode += ";current height: {height}\n".format(height=current_height)
|
||||||
else:
|
else:
|
||||||
prepend_gcode += ";current layer: {layer}\n".format(layer = current_layer)
|
prepend_gcode += ";current layer: {layer}\n".format(layer=current_layer)
|
||||||
|
|
||||||
# Retraction
|
# Retraction
|
||||||
prepend_gcode += self.putValue(M = 83) + "\n"
|
prepend_gcode += self.putValue(M=83) + "\n"
|
||||||
if retraction_amount != 0:
|
if retraction_amount != 0:
|
||||||
prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = retraction_speed * 60) + "\n"
|
prepend_gcode += self.putValue(G=1, E=-retraction_amount, F=retraction_speed * 60) + "\n"
|
||||||
|
|
||||||
# Move the head away
|
# Move the head away
|
||||||
prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + "\n"
|
prepend_gcode += self.putValue(G=1, Z=current_z + 1, F=300) + "\n"
|
||||||
prepend_gcode += self.putValue(G = 1, X = park_x, Y = park_y, F = 9000) + "\n"
|
|
||||||
|
# This line should be ok
|
||||||
|
prepend_gcode += self.putValue(G=1, X=park_x, Y=park_y, F=9000) + "\n"
|
||||||
|
|
||||||
if current_z < 15:
|
if current_z < 15:
|
||||||
prepend_gcode += self.putValue(G = 1, Z = 15, F = 300) + "\n"
|
prepend_gcode += self.putValue(G=1, Z=15, F=300) + "\n"
|
||||||
|
|
||||||
# Disable the E steppers
|
# Disable the E steppers
|
||||||
prepend_gcode += self.putValue(M = 84, E = 0) + "\n"
|
prepend_gcode += self.putValue(M=84, E=0) + "\n"
|
||||||
|
|
||||||
# Set extruder standby temperature
|
# Set extruder standby temperature
|
||||||
prepend_gcode += self.putValue(M = 104, S = standby_temperature) + "; standby temperature\n"
|
prepend_gcode += self.putValue(M=104, S=standby_temperature) + "; standby temperature\n"
|
||||||
|
|
||||||
# Wait till the user continues printing
|
# Wait till the user continues printing
|
||||||
prepend_gcode += self.putValue(M = 0) + ";Do the actual pause\n"
|
prepend_gcode += self.putValue(M=0) + ";Do the actual pause\n"
|
||||||
|
|
||||||
# Set extruder resume temperature
|
# Set extruder resume temperature
|
||||||
prepend_gcode += self.putValue(M = 109, S = resume_temperature) + "; resume temperature\n"
|
prepend_gcode += self.putValue(M=109, S=resume_temperature) + "; resume temperature\n"
|
||||||
|
|
||||||
# Push the filament back,
|
# Push the filament back,
|
||||||
if retraction_amount != 0:
|
if retraction_amount != 0:
|
||||||
prepend_gcode += self.putValue(G = 1, E = retraction_amount, F = retraction_speed * 60) + "\n"
|
prepend_gcode += self.putValue(G=1, E=retraction_amount, F=retraction_speed * 60) + "\n"
|
||||||
|
|
||||||
# Optionally extrude material
|
# Optionally extrude material
|
||||||
if extrude_amount != 0:
|
if extrude_amount != 0:
|
||||||
prepend_gcode += self.putValue(G = 1, E = extrude_amount, F = extrude_speed * 60) + "\n"
|
prepend_gcode += self.putValue(G=1, E=extrude_amount, F=extrude_speed * 60) + "\n"
|
||||||
|
|
||||||
# and retract again, the properly primes the nozzle
|
# and retract again, the properly primes the nozzle
|
||||||
# when changing filament.
|
# when changing filament.
|
||||||
if retraction_amount != 0:
|
if retraction_amount != 0:
|
||||||
prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = retraction_speed * 60) + "\n"
|
prepend_gcode += self.putValue(G=1, E=-retraction_amount, F=retraction_speed * 60) + "\n"
|
||||||
|
|
||||||
# Move the head back
|
# Move the head back
|
||||||
prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + "\n"
|
prepend_gcode += self.putValue(G=1, Z=current_z + 1, F=300) + "\n"
|
||||||
prepend_gcode += self.putValue(G = 1, X = x, Y = y, F = 9000) + "\n"
|
prepend_gcode += self.putValue(G=1, X=x, Y=y, F=9000) + "\n"
|
||||||
if retraction_amount != 0:
|
if retraction_amount != 0:
|
||||||
prepend_gcode += self.putValue(G = 1, E = retraction_amount, F = retraction_speed * 60) + "\n"
|
prepend_gcode += self.putValue(G=1, E=retraction_amount, F=retraction_speed * 60) + "\n"
|
||||||
prepend_gcode += self.putValue(G = 1, F = 9000) + "\n"
|
prepend_gcode += self.putValue(G=1, F=9000) + "\n"
|
||||||
prepend_gcode += self.putValue(M = 82) + "\n"
|
prepend_gcode += self.putValue(M=82) + "\n"
|
||||||
|
|
||||||
# reset extrude value to pre pause value
|
# reset extrude value to pre pause value
|
||||||
prepend_gcode += self.putValue(G = 92, E = current_e) + "\n"
|
prepend_gcode += self.putValue(G=92, E=current_e) + "\n"
|
||||||
|
|
||||||
layer = prepend_gcode + layer
|
layer = prepend_gcode + layer
|
||||||
|
|
||||||
|
|
||||||
# Override the data of this layer with the
|
# Override the data of this layer with the
|
||||||
# modified data
|
# modified data
|
||||||
data[index] = layer
|
data[index] = layer
|
||||||
|
|
|
@ -14,5 +14,6 @@ class PrepareStage(CuraStage):
|
||||||
Application.getInstance().engineCreatedSignal.connect(self._engineCreated)
|
Application.getInstance().engineCreatedSignal.connect(self._engineCreated)
|
||||||
|
|
||||||
def _engineCreated(self):
|
def _engineCreated(self):
|
||||||
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "Sidebar.qml")
|
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles),
|
||||||
|
"PrepareSidebar.qml")
|
||||||
self.addDisplayComponent("sidebar", sidebar_component_path)
|
self.addDisplayComponent("sidebar", sidebar_component_path)
|
||||||
|
|
|
@ -24,17 +24,26 @@ from .PackagesModel import PackagesModel
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
## The Toolbox class is responsible of communicating with the server through the API
|
## The Toolbox class is responsible of communicating with the server through the API
|
||||||
class Toolbox(QObject, Extension):
|
class Toolbox(QObject, Extension):
|
||||||
|
|
||||||
|
DEFAULT_PACKAGES_API_ROOT = "https://api.ultimaker.com"
|
||||||
|
|
||||||
def __init__(self, parent=None) -> None:
|
def __init__(self, parent=None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self._application = Application.getInstance()
|
self._application = Application.getInstance()
|
||||||
self._package_manager = None
|
self._package_manager = None
|
||||||
self._plugin_registry = Application.getInstance().getPluginRegistry()
|
self._plugin_registry = Application.getInstance().getPluginRegistry()
|
||||||
|
self._packages_api_root = self._getPackagesApiRoot()
|
||||||
self._packages_version = self._getPackagesVersion()
|
self._packages_version = self._getPackagesVersion()
|
||||||
self._api_version = 1
|
self._api_version = 1
|
||||||
self._api_url = "https://api.ultimaker.com/cura-packages/v{api_version}/cura/v{package_version}".format( api_version = self._api_version, package_version = self._packages_version)
|
self._api_url = "{api_root}/cura-packages/v{api_version}/cura/v{package_version}".format(
|
||||||
|
api_root = self._packages_api_root,
|
||||||
|
api_version = self._api_version,
|
||||||
|
package_version = self._packages_version
|
||||||
|
)
|
||||||
|
|
||||||
# Network:
|
# Network:
|
||||||
self._get_packages_request = None
|
self._get_packages_request = None
|
||||||
|
@ -153,6 +162,15 @@ class Toolbox(QObject, Extension):
|
||||||
def _onAppInitialized(self) -> None:
|
def _onAppInitialized(self) -> None:
|
||||||
self._package_manager = Application.getInstance().getCuraPackageManager()
|
self._package_manager = Application.getInstance().getCuraPackageManager()
|
||||||
|
|
||||||
|
# Get the API root for the packages API depending on Cura version settings.
|
||||||
|
def _getPackagesApiRoot(self) -> str:
|
||||||
|
if not hasattr(cura, "CuraVersion"):
|
||||||
|
return self.DEFAULT_PACKAGES_API_ROOT
|
||||||
|
if not hasattr(cura.CuraVersion, "CuraPackagesApiRoot"):
|
||||||
|
return self.DEFAULT_PACKAGES_API_ROOT
|
||||||
|
return cura.CuraVersion.CuraPackagesApiRoot
|
||||||
|
|
||||||
|
# Get the packages version depending on Cura version settings.
|
||||||
def _getPackagesVersion(self) -> int:
|
def _getPackagesVersion(self) -> int:
|
||||||
if not hasattr(cura, "CuraVersion"):
|
if not hasattr(cura, "CuraVersion"):
|
||||||
return self._plugin_registry.APIVersion
|
return self._plugin_registry.APIVersion
|
||||||
|
|
|
@ -308,7 +308,21 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
||||||
if b"ok T:" in line or line.startswith(b"T:") or b"ok B:" in line or line.startswith(b"B:"): # Temperature message. 'T:' for extruder and 'B:' for bed
|
if b"ok T:" in line or line.startswith(b"T:") or b"ok B:" in line or line.startswith(b"B:"): # Temperature message. 'T:' for extruder and 'B:' for bed
|
||||||
extruder_temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line)
|
extruder_temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line)
|
||||||
# Update all temperature values
|
# Update all temperature values
|
||||||
for match, extruder in zip(extruder_temperature_matches, self._printers[0].extruders):
|
matched_extruder_nrs = []
|
||||||
|
for match in extruder_temperature_matches:
|
||||||
|
extruder_nr = 0
|
||||||
|
if match[0] != b"":
|
||||||
|
extruder_nr = int(match[0])
|
||||||
|
|
||||||
|
if extruder_nr in matched_extruder_nrs:
|
||||||
|
continue
|
||||||
|
matched_extruder_nrs.append(extruder_nr)
|
||||||
|
|
||||||
|
if extruder_nr >= len(self._printers[0].extruders):
|
||||||
|
Logger.log("w", "Printer reports more temperatures than the number of configured extruders")
|
||||||
|
continue
|
||||||
|
|
||||||
|
extruder = self._printers[0].extruders[extruder_nr]
|
||||||
if match[1]:
|
if match[1]:
|
||||||
extruder.updateHotendTemperature(float(match[1]))
|
extruder.updateHotendTemperature(float(match[1]))
|
||||||
if match[2]:
|
if match[2]:
|
||||||
|
|
|
@ -60,7 +60,7 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
|
||||||
self._check_updates = True
|
self._check_updates = True
|
||||||
self._update_thread.start()
|
self._update_thread.start()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self, store_data: bool = True):
|
||||||
self._check_updates = False
|
self._check_updates = False
|
||||||
|
|
||||||
def _onConnectionStateChanged(self, serial_port):
|
def _onConnectionStateChanged(self, serial_port):
|
||||||
|
@ -79,10 +79,11 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
|
||||||
if container_stack is None:
|
if container_stack is None:
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
continue
|
continue
|
||||||
|
port_list = [] # Just an empty list; all USB devices will be removed.
|
||||||
if container_stack.getMetaDataEntry("supports_usb_connection"):
|
if container_stack.getMetaDataEntry("supports_usb_connection"):
|
||||||
port_list = self.getSerialPortList(only_list_usb=True)
|
machine_file_formats = [file_type.strip() for file_type in container_stack.getMetaDataEntry("file_formats").split(";")]
|
||||||
else:
|
if "text/x-gcode" in machine_file_formats:
|
||||||
port_list = [] # Just use an empty list; all USB devices will be removed.
|
port_list = self.getSerialPortList(only_list_usb=True)
|
||||||
self._addRemovePorts(port_list)
|
self._addRemovePorts(port_list)
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
|
|
|
@ -33,23 +33,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AutoSave": {
|
|
||||||
"package_info": {
|
|
||||||
"package_id": "AutoSave",
|
|
||||||
"package_type": "plugin",
|
|
||||||
"display_name": "Auto-Save",
|
|
||||||
"description": "Automatically saves Preferences, Machines and Profiles after changes.",
|
|
||||||
"package_version": "1.0.0",
|
|
||||||
"cura_version": 4,
|
|
||||||
"website": "https://ultimaker.com",
|
|
||||||
"author": {
|
|
||||||
"author_id": "Ultimaker",
|
|
||||||
"display_name": "Ultimaker B.V.",
|
|
||||||
"email": "plugins@ultimaker.com",
|
|
||||||
"website": "https://ultimaker.com"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ChangeLogPlugin": {
|
"ChangeLogPlugin": {
|
||||||
"package_info": {
|
"package_info": {
|
||||||
"package_id": "ChangeLogPlugin",
|
"package_id": "ChangeLogPlugin",
|
||||||
|
|
|
@ -3324,6 +3324,16 @@
|
||||||
"settable_per_mesh": false,
|
"settable_per_mesh": false,
|
||||||
"settable_per_extruder": true
|
"settable_per_extruder": true
|
||||||
},
|
},
|
||||||
|
"travel_avoid_supports":
|
||||||
|
{
|
||||||
|
"label": "Avoid Supports When Traveling",
|
||||||
|
"description": "The nozzle avoids already printed supports when traveling. This option is only available when combing is enabled.",
|
||||||
|
"type": "bool",
|
||||||
|
"default_value": false,
|
||||||
|
"enabled": "resolveOrValue('retraction_combing') != 'off' and travel_avoid_other_parts",
|
||||||
|
"settable_per_mesh": false,
|
||||||
|
"settable_per_extruder": true
|
||||||
|
},
|
||||||
"travel_avoid_distance":
|
"travel_avoid_distance":
|
||||||
{
|
{
|
||||||
"label": "Travel Avoid Distance",
|
"label": "Travel Avoid Distance",
|
||||||
|
|
|
@ -3106,6 +3106,18 @@ msgid ""
|
||||||
"available when combing is enabled."
|
"available when combing is enabled."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: fdmprinter.def.json
|
||||||
|
msgctxt "travel_avoid_supports label"
|
||||||
|
msgid "Avoid Supports When Traveling"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: fdmprinter.def.json
|
||||||
|
msgctxt "travel_avoid_supports description"
|
||||||
|
msgid ""
|
||||||
|
"The nozzle avoids already printed supports when traveling. This option is only "
|
||||||
|
"available when combing is enabled."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: fdmprinter.def.json
|
#: fdmprinter.def.json
|
||||||
msgctxt "travel_avoid_distance label"
|
msgctxt "travel_avoid_distance label"
|
||||||
msgid "Travel Avoid Distance"
|
msgid "Travel Avoid Distance"
|
||||||
|
|
|
@ -15,6 +15,8 @@ Item
|
||||||
id: base;
|
id: base;
|
||||||
UM.I18nCatalog { id: catalog; name:"cura"}
|
UM.I18nCatalog { id: catalog; name:"cura"}
|
||||||
|
|
||||||
|
height: childrenRect.height + UM.Theme.getSize("sidebar_margin").height
|
||||||
|
|
||||||
property bool printerConnected: Cura.MachineManager.printerConnected
|
property bool printerConnected: Cura.MachineManager.printerConnected
|
||||||
property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands
|
property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands
|
||||||
property var activePrinter: printerConnected ? Cura.MachineManager.printerOutputDevices[0].activePrinter : null
|
property var activePrinter: printerConnected ? Cura.MachineManager.printerOutputDevices[0].activePrinter : null
|
||||||
|
|
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"))
|
scaleToFitCheckbox.checked = boolCheck(UM.Preferences.getValue("mesh/scale_to_fit"))
|
||||||
UM.Preferences.resetPreference("mesh/scale_tiny_meshes")
|
UM.Preferences.resetPreference("mesh/scale_tiny_meshes")
|
||||||
scaleTinyCheckbox.checked = boolCheck(UM.Preferences.getValue("mesh/scale_tiny_meshes"))
|
scaleTinyCheckbox.checked = boolCheck(UM.Preferences.getValue("mesh/scale_tiny_meshes"))
|
||||||
|
UM.Preferences.resetPreference("cura/select_models_on_load")
|
||||||
|
selectModelsOnLoadCheckbox.checked = boolCheck(UM.Preferences.getValue("cura/select_models_on_load"))
|
||||||
UM.Preferences.resetPreference("cura/jobname_prefix")
|
UM.Preferences.resetPreference("cura/jobname_prefix")
|
||||||
prefixJobNameCheckbox.checked = boolCheck(UM.Preferences.getValue("cura/jobname_prefix"))
|
prefixJobNameCheckbox.checked = boolCheck(UM.Preferences.getValue("cura/jobname_prefix"))
|
||||||
UM.Preferences.resetPreference("view/show_overhang");
|
UM.Preferences.resetPreference("view/show_overhang");
|
||||||
|
@ -498,6 +500,21 @@ UM.PreferencesPage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UM.TooltipArea
|
||||||
|
{
|
||||||
|
width: childrenRect.width
|
||||||
|
height: childrenRect.height
|
||||||
|
text: catalog.i18nc("@info:tooltip","Should models be selected after they are loaded?")
|
||||||
|
|
||||||
|
CheckBox
|
||||||
|
{
|
||||||
|
id: selectModelsOnLoadCheckbox
|
||||||
|
text: catalog.i18nc("@option:check","Select models when loaded")
|
||||||
|
checked: boolCheck(UM.Preferences.getValue("cura/select_models_on_load"))
|
||||||
|
onCheckedChanged: UM.Preferences.setValue("cura/select_models_on_load", checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
UM.TooltipArea
|
UM.TooltipArea
|
||||||
{
|
{
|
||||||
width: childrenRect.width
|
width: childrenRect.width
|
||||||
|
|
|
@ -24,8 +24,6 @@ Rectangle
|
||||||
property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands
|
property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands
|
||||||
property var connectedPrinter: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null
|
property var connectedPrinter: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null
|
||||||
|
|
||||||
property bool monitoringPrint: UM.Controller.activeStage.stageId == "MonitorStage"
|
|
||||||
|
|
||||||
property variant printDuration: PrintInformation.currentPrintTime
|
property variant printDuration: PrintInformation.currentPrintTime
|
||||||
property variant printMaterialLengths: PrintInformation.materialLengths
|
property variant printMaterialLengths: PrintInformation.materialLengths
|
||||||
property variant printMaterialWeights: PrintInformation.materialWeights
|
property variant printMaterialWeights: PrintInformation.materialWeights
|
||||||
|
@ -120,7 +118,7 @@ Rectangle
|
||||||
SidebarHeader {
|
SidebarHeader {
|
||||||
id: header
|
id: header
|
||||||
width: parent.width
|
width: parent.width
|
||||||
visible: !hideSettings && (machineExtruderCount.properties.value > 1 || Cura.MachineManager.hasMaterials || Cura.MachineManager.hasVariants) && !monitoringPrint
|
visible: !hideSettings && (machineExtruderCount.properties.value > 1 || Cura.MachineManager.hasMaterials || Cura.MachineManager.hasVariants)
|
||||||
anchors.top: machineSelection.bottom
|
anchors.top: machineSelection.bottom
|
||||||
|
|
||||||
onShowTooltip: base.showTooltip(item, location, text)
|
onShowTooltip: base.showTooltip(item, location, text)
|
||||||
|
@ -158,7 +156,7 @@ Rectangle
|
||||||
width: Math.round(parent.width * 0.45)
|
width: Math.round(parent.width * 0.45)
|
||||||
font: UM.Theme.getFont("large")
|
font: UM.Theme.getFont("large")
|
||||||
color: UM.Theme.getColor("text")
|
color: UM.Theme.getColor("text")
|
||||||
visible: !monitoringPrint && !hideView
|
visible: !hideView
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settings mode selection toggle
|
// Settings mode selection toggle
|
||||||
|
@ -185,7 +183,7 @@ Rectangle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
visible: !monitoringPrint && !hideSettings && !hideView
|
visible: !hideSettings && !hideView
|
||||||
|
|
||||||
Component
|
Component
|
||||||
{
|
{
|
||||||
|
@ -282,7 +280,7 @@ Rectangle
|
||||||
anchors.topMargin: UM.Theme.getSize("sidebar_margin").height
|
anchors.topMargin: UM.Theme.getSize("sidebar_margin").height
|
||||||
anchors.left: base.left
|
anchors.left: base.left
|
||||||
anchors.right: base.right
|
anchors.right: base.right
|
||||||
visible: !monitoringPrint && !hideSettings
|
visible: !hideSettings
|
||||||
|
|
||||||
replaceEnter: Transition {
|
replaceEnter: Transition {
|
||||||
PropertyAnimation {
|
PropertyAnimation {
|
||||||
|
@ -305,47 +303,11 @@ Rectangle
|
||||||
|
|
||||||
Loader
|
Loader
|
||||||
{
|
{
|
||||||
id: controlItem
|
|
||||||
anchors.bottom: footerSeparator.top
|
anchors.bottom: footerSeparator.top
|
||||||
anchors.top: monitoringPrint ? machineSelection.bottom : headerSeparator.bottom
|
anchors.top: headerSeparator.bottom
|
||||||
anchors.left: base.left
|
anchors.left: base.left
|
||||||
anchors.right: base.right
|
anchors.right: base.right
|
||||||
sourceComponent:
|
source: "SidebarContents.qml"
|
||||||
{
|
|
||||||
if(monitoringPrint && connectedPrinter != null)
|
|
||||||
{
|
|
||||||
if(connectedPrinter.controlItem != null)
|
|
||||||
{
|
|
||||||
return connectedPrinter.controlItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
anchors.bottom: footerSeparator.top
|
|
||||||
anchors.top: monitoringPrint ? machineSelection.bottom : headerSeparator.bottom
|
|
||||||
anchors.left: base.left
|
|
||||||
anchors.right: base.right
|
|
||||||
source:
|
|
||||||
{
|
|
||||||
if(controlItem.sourceComponent == null)
|
|
||||||
{
|
|
||||||
if(monitoringPrint)
|
|
||||||
{
|
|
||||||
return "PrintMonitor.qml"
|
|
||||||
} else
|
|
||||||
{
|
|
||||||
return "SidebarContents.qml"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle
|
Rectangle
|
||||||
|
@ -367,7 +329,6 @@ Rectangle
|
||||||
anchors.bottomMargin: UM.Theme.getSize("sidebar_margin").height
|
anchors.bottomMargin: UM.Theme.getSize("sidebar_margin").height
|
||||||
height: timeDetails.height + costSpec.height
|
height: timeDetails.height + costSpec.height
|
||||||
width: base.width - (saveButton.buttonRowWidth + UM.Theme.getSize("sidebar_margin").width)
|
width: base.width - (saveButton.buttonRowWidth + UM.Theme.getSize("sidebar_margin").width)
|
||||||
visible: !monitoringPrint
|
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
Label
|
Label
|
||||||
|
@ -570,8 +531,7 @@ Rectangle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveButton and MonitorButton are actually the bottom footer panels.
|
// SaveButton is actually the bottom footer panel.
|
||||||
// "!monitoringPrint" currently means "show-settings-mode"
|
|
||||||
SaveButton
|
SaveButton
|
||||||
{
|
{
|
||||||
id: saveButton
|
id: saveButton
|
||||||
|
@ -579,17 +539,6 @@ Rectangle
|
||||||
anchors.top: footerSeparator.bottom
|
anchors.top: footerSeparator.bottom
|
||||||
anchors.topMargin: UM.Theme.getSize("sidebar_margin").height
|
anchors.topMargin: UM.Theme.getSize("sidebar_margin").height
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
visible: !monitoringPrint
|
|
||||||
}
|
|
||||||
|
|
||||||
MonitorButton
|
|
||||||
{
|
|
||||||
id: monitorButton
|
|
||||||
implicitWidth: base.width
|
|
||||||
anchors.top: footerSeparator.bottom
|
|
||||||
anchors.topMargin: UM.Theme.getSize("sidebar_margin").height
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
visible: monitoringPrint
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SidebarTooltip
|
SidebarTooltip
|
|
@ -17,7 +17,17 @@ Column
|
||||||
property int currentExtruderIndex: Cura.ExtruderManager.activeExtruderIndex;
|
property int currentExtruderIndex: Cura.ExtruderManager.activeExtruderIndex;
|
||||||
property bool currentExtruderVisible: extrudersList.visible;
|
property bool currentExtruderVisible: extrudersList.visible;
|
||||||
property bool printerConnected: Cura.MachineManager.printerConnected
|
property bool printerConnected: Cura.MachineManager.printerConnected
|
||||||
property bool hasManyPrinterTypes: printerConnected ? Cura.MachineManager.printerOutputDevices[0].connectedPrintersTypeCount.length > 1 : false
|
property bool hasManyPrinterTypes:
|
||||||
|
{
|
||||||
|
if (printerConnected)
|
||||||
|
{
|
||||||
|
if (Cura.MachineManager.printerOutputDevices[0].connectedPrintersTypeCount != null)
|
||||||
|
{
|
||||||
|
return Cura.MachineManager.printerOutputDevices[0].connectedPrintersTypeCount.length > 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
property bool buildplateCompatibilityError: !Cura.MachineManager.variantBuildplateCompatible && !Cura.MachineManager.variantBuildplateUsable
|
property bool buildplateCompatibilityError: !Cura.MachineManager.variantBuildplateCompatible && !Cura.MachineManager.variantBuildplateUsable
|
||||||
property bool buildplateCompatibilityWarning: Cura.MachineManager.variantBuildplateUsable
|
property bool buildplateCompatibilityWarning: Cura.MachineManager.variantBuildplateUsable
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,7 @@ jerk_enabled
|
||||||
[travel]
|
[travel]
|
||||||
retraction_combing
|
retraction_combing
|
||||||
travel_avoid_other_parts
|
travel_avoid_other_parts
|
||||||
|
travel_avoid_supports
|
||||||
travel_avoid_distance
|
travel_avoid_distance
|
||||||
retraction_hop_enabled
|
retraction_hop_enabled
|
||||||
retraction_hop_only_when_collides
|
retraction_hop_only_when_collides
|
||||||
|
|
|
@ -187,6 +187,7 @@ jerk_skirt_brim
|
||||||
retraction_combing
|
retraction_combing
|
||||||
travel_retract_before_outer_wall
|
travel_retract_before_outer_wall
|
||||||
travel_avoid_other_parts
|
travel_avoid_other_parts
|
||||||
|
travel_avoid_supports
|
||||||
travel_avoid_distance
|
travel_avoid_distance
|
||||||
start_layers_at_same_position
|
start_layers_at_same_position
|
||||||
layer_start_x
|
layer_start_x
|
||||||
|
|
|
@ -4,9 +4,27 @@ from cura.Arranging.Arrange import Arrange
|
||||||
from cura.Arranging.ShapeArray import ShapeArray
|
from cura.Arranging.ShapeArray import ShapeArray
|
||||||
|
|
||||||
|
|
||||||
def gimmeShapeArray():
|
## Triangle of area 12
|
||||||
vertices = numpy.array([[-3, 1], [3, 1], [0, -3]])
|
def gimmeTriangle():
|
||||||
shape_arr = ShapeArray.fromPolygon(vertices)
|
return numpy.array([[-3, 1], [3, 1], [0, -3]], dtype=numpy.int32)
|
||||||
|
|
||||||
|
|
||||||
|
## Boring square
|
||||||
|
def gimmeSquare():
|
||||||
|
return numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32)
|
||||||
|
|
||||||
|
|
||||||
|
## Triangle of area 12
|
||||||
|
def gimmeShapeArray(scale = 1.0):
|
||||||
|
vertices = gimmeTriangle()
|
||||||
|
shape_arr = ShapeArray.fromPolygon(vertices, scale = scale)
|
||||||
|
return shape_arr
|
||||||
|
|
||||||
|
|
||||||
|
## Boring square
|
||||||
|
def gimmeShapeArraySquare(scale = 1.0):
|
||||||
|
vertices = gimmeSquare()
|
||||||
|
shape_arr = ShapeArray.fromPolygon(vertices, scale = scale)
|
||||||
return shape_arr
|
return shape_arr
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,9 +38,48 @@ def test_smoke_ShapeArray():
|
||||||
shape_arr = gimmeShapeArray()
|
shape_arr = gimmeShapeArray()
|
||||||
|
|
||||||
|
|
||||||
|
## Test ShapeArray
|
||||||
|
def test_ShapeArray():
|
||||||
|
scale = 1
|
||||||
|
ar = Arrange(16, 16, 8, 8, scale = scale)
|
||||||
|
ar.centerFirst()
|
||||||
|
|
||||||
|
shape_arr = gimmeShapeArray(scale)
|
||||||
|
print(shape_arr.arr)
|
||||||
|
count = len(numpy.where(shape_arr.arr == 1)[0])
|
||||||
|
print(count)
|
||||||
|
assert count >= 10 # should approach 12
|
||||||
|
|
||||||
|
|
||||||
|
## Test ShapeArray with scaling
|
||||||
|
def test_ShapeArray_scaling():
|
||||||
|
scale = 2
|
||||||
|
ar = Arrange(16, 16, 8, 8, scale = scale)
|
||||||
|
ar.centerFirst()
|
||||||
|
|
||||||
|
shape_arr = gimmeShapeArray(scale)
|
||||||
|
print(shape_arr.arr)
|
||||||
|
count = len(numpy.where(shape_arr.arr == 1)[0])
|
||||||
|
print(count)
|
||||||
|
assert count >= 40 # should approach 2*2*12 = 48
|
||||||
|
|
||||||
|
|
||||||
|
## Test ShapeArray with scaling
|
||||||
|
def test_ShapeArray_scaling2():
|
||||||
|
scale = 0.5
|
||||||
|
ar = Arrange(16, 16, 8, 8, scale = scale)
|
||||||
|
ar.centerFirst()
|
||||||
|
|
||||||
|
shape_arr = gimmeShapeArray(scale)
|
||||||
|
print(shape_arr.arr)
|
||||||
|
count = len(numpy.where(shape_arr.arr == 1)[0])
|
||||||
|
print(count)
|
||||||
|
assert count >= 1 # should approach 3, but it can be inaccurate due to pixel rounding
|
||||||
|
|
||||||
|
|
||||||
## Test centerFirst
|
## Test centerFirst
|
||||||
def test_centerFirst():
|
def test_centerFirst():
|
||||||
ar = Arrange(300, 300, 150, 150)
|
ar = Arrange(300, 300, 150, 150, scale = 1)
|
||||||
ar.centerFirst()
|
ar.centerFirst()
|
||||||
assert ar._priority[150][150] < ar._priority[170][150]
|
assert ar._priority[150][150] < ar._priority[170][150]
|
||||||
assert ar._priority[150][150] < ar._priority[150][170]
|
assert ar._priority[150][150] < ar._priority[150][170]
|
||||||
|
@ -32,19 +89,39 @@ def test_centerFirst():
|
||||||
assert ar._priority[150][150] < ar._priority[130][130]
|
assert ar._priority[150][150] < ar._priority[130][130]
|
||||||
|
|
||||||
|
|
||||||
|
## Test centerFirst
|
||||||
|
def test_centerFirst_rectangular():
|
||||||
|
ar = Arrange(400, 300, 200, 150, scale = 1)
|
||||||
|
ar.centerFirst()
|
||||||
|
assert ar._priority[150][200] < ar._priority[150][220]
|
||||||
|
assert ar._priority[150][200] < ar._priority[170][200]
|
||||||
|
assert ar._priority[150][200] < ar._priority[170][220]
|
||||||
|
assert ar._priority[150][200] < ar._priority[180][150]
|
||||||
|
assert ar._priority[150][200] < ar._priority[130][200]
|
||||||
|
assert ar._priority[150][200] < ar._priority[130][180]
|
||||||
|
|
||||||
|
|
||||||
|
## Test centerFirst
|
||||||
|
def test_centerFirst_rectangular():
|
||||||
|
ar = Arrange(10, 20, 5, 10, scale = 1)
|
||||||
|
ar.centerFirst()
|
||||||
|
print(ar._priority)
|
||||||
|
assert ar._priority[10][5] < ar._priority[10][7]
|
||||||
|
|
||||||
|
|
||||||
## Test backFirst
|
## Test backFirst
|
||||||
def test_backFirst():
|
def test_backFirst():
|
||||||
ar = Arrange(300, 300, 150, 150)
|
ar = Arrange(300, 300, 150, 150, scale = 1)
|
||||||
ar.backFirst()
|
ar.backFirst()
|
||||||
assert ar._priority[150][150] < ar._priority[150][170]
|
assert ar._priority[150][150] < ar._priority[170][150]
|
||||||
assert ar._priority[150][150] < ar._priority[170][170]
|
assert ar._priority[150][150] < ar._priority[170][170]
|
||||||
assert ar._priority[150][150] > ar._priority[150][130]
|
assert ar._priority[150][150] > ar._priority[130][150]
|
||||||
assert ar._priority[150][150] > ar._priority[130][130]
|
assert ar._priority[150][150] > ar._priority[130][130]
|
||||||
|
|
||||||
|
|
||||||
## See if the result of bestSpot has the correct form
|
## See if the result of bestSpot has the correct form
|
||||||
def test_smoke_bestSpot():
|
def test_smoke_bestSpot():
|
||||||
ar = Arrange(30, 30, 15, 15)
|
ar = Arrange(30, 30, 15, 15, scale = 1)
|
||||||
ar.centerFirst()
|
ar.centerFirst()
|
||||||
|
|
||||||
shape_arr = gimmeShapeArray()
|
shape_arr = gimmeShapeArray()
|
||||||
|
@ -55,6 +132,113 @@ def test_smoke_bestSpot():
|
||||||
assert hasattr(best_spot, "priority")
|
assert hasattr(best_spot, "priority")
|
||||||
|
|
||||||
|
|
||||||
|
## Real life test
|
||||||
|
def test_bestSpot():
|
||||||
|
ar = Arrange(16, 16, 8, 8, scale = 1)
|
||||||
|
ar.centerFirst()
|
||||||
|
|
||||||
|
shape_arr = gimmeShapeArray()
|
||||||
|
best_spot = ar.bestSpot(shape_arr)
|
||||||
|
assert best_spot.x == 0
|
||||||
|
assert best_spot.y == 0
|
||||||
|
ar.place(best_spot.x, best_spot.y, shape_arr)
|
||||||
|
|
||||||
|
# Place object a second time
|
||||||
|
best_spot = ar.bestSpot(shape_arr)
|
||||||
|
assert best_spot.x is not None # we found a location
|
||||||
|
assert best_spot.x != 0 or best_spot.y != 0 # it can't be on the same location
|
||||||
|
ar.place(best_spot.x, best_spot.y, shape_arr)
|
||||||
|
|
||||||
|
print(ar._occupied) # For debugging
|
||||||
|
|
||||||
|
|
||||||
|
## Real life test rectangular build plate
|
||||||
|
def test_bestSpot_rectangular_build_plate():
|
||||||
|
ar = Arrange(16, 40, 8, 20, scale = 1)
|
||||||
|
ar.centerFirst()
|
||||||
|
|
||||||
|
shape_arr = gimmeShapeArray()
|
||||||
|
best_spot = ar.bestSpot(shape_arr)
|
||||||
|
ar.place(best_spot.x, best_spot.y, shape_arr)
|
||||||
|
assert best_spot.x == 0
|
||||||
|
assert best_spot.y == 0
|
||||||
|
|
||||||
|
# Place object a second time
|
||||||
|
best_spot2 = ar.bestSpot(shape_arr)
|
||||||
|
assert best_spot2.x is not None # we found a location
|
||||||
|
assert best_spot2.x != 0 or best_spot2.y != 0 # it can't be on the same location
|
||||||
|
ar.place(best_spot2.x, best_spot2.y, shape_arr)
|
||||||
|
|
||||||
|
# Place object a 3rd time
|
||||||
|
best_spot3 = ar.bestSpot(shape_arr)
|
||||||
|
assert best_spot3.x is not None # we found a location
|
||||||
|
assert best_spot3.x != best_spot.x or best_spot3.y != best_spot.y # it can't be on the same location
|
||||||
|
assert best_spot3.x != best_spot2.x or best_spot3.y != best_spot2.y # it can't be on the same location
|
||||||
|
ar.place(best_spot3.x, best_spot3.y, shape_arr)
|
||||||
|
|
||||||
|
best_spot_x = ar.bestSpot(shape_arr)
|
||||||
|
ar.place(best_spot_x.x, best_spot_x.y, shape_arr)
|
||||||
|
|
||||||
|
best_spot_x = ar.bestSpot(shape_arr)
|
||||||
|
ar.place(best_spot_x.x, best_spot_x.y, shape_arr)
|
||||||
|
|
||||||
|
best_spot_x = ar.bestSpot(shape_arr)
|
||||||
|
ar.place(best_spot_x.x, best_spot_x.y, shape_arr)
|
||||||
|
|
||||||
|
print(ar._occupied) # For debugging
|
||||||
|
|
||||||
|
|
||||||
|
## Real life test
|
||||||
|
def test_bestSpot_scale():
|
||||||
|
scale = 0.5
|
||||||
|
ar = Arrange(16, 16, 8, 8, scale = scale)
|
||||||
|
ar.centerFirst()
|
||||||
|
|
||||||
|
shape_arr = gimmeShapeArray(scale)
|
||||||
|
best_spot = ar.bestSpot(shape_arr)
|
||||||
|
assert best_spot.x == 0
|
||||||
|
assert best_spot.y == 0
|
||||||
|
ar.place(best_spot.x, best_spot.y, shape_arr)
|
||||||
|
|
||||||
|
print(ar._occupied)
|
||||||
|
|
||||||
|
# Place object a second time
|
||||||
|
best_spot = ar.bestSpot(shape_arr)
|
||||||
|
assert best_spot.x is not None # we found a location
|
||||||
|
assert best_spot.x != 0 or best_spot.y != 0 # it can't be on the same location
|
||||||
|
ar.place(best_spot.x, best_spot.y, shape_arr)
|
||||||
|
|
||||||
|
print(ar._occupied) # For debugging
|
||||||
|
|
||||||
|
|
||||||
|
## Real life test
|
||||||
|
def test_bestSpot_scale_rectangular():
|
||||||
|
scale = 0.5
|
||||||
|
ar = Arrange(16, 40, 8, 20, scale = scale)
|
||||||
|
ar.centerFirst()
|
||||||
|
|
||||||
|
shape_arr = gimmeShapeArray(scale)
|
||||||
|
|
||||||
|
shape_arr_square = gimmeShapeArraySquare(scale)
|
||||||
|
best_spot = ar.bestSpot(shape_arr_square)
|
||||||
|
assert best_spot.x == 0
|
||||||
|
assert best_spot.y == 0
|
||||||
|
ar.place(best_spot.x, best_spot.y, shape_arr_square)
|
||||||
|
|
||||||
|
print(ar._occupied)
|
||||||
|
|
||||||
|
# Place object a second time
|
||||||
|
best_spot = ar.bestSpot(shape_arr)
|
||||||
|
assert best_spot.x is not None # we found a location
|
||||||
|
assert best_spot.x != 0 or best_spot.y != 0 # it can't be on the same location
|
||||||
|
ar.place(best_spot.x, best_spot.y, shape_arr)
|
||||||
|
|
||||||
|
best_spot = ar.bestSpot(shape_arr_square)
|
||||||
|
ar.place(best_spot.x, best_spot.y, shape_arr_square)
|
||||||
|
|
||||||
|
print(ar._occupied) # For debugging
|
||||||
|
|
||||||
|
|
||||||
## Try to place an object and see if something explodes
|
## Try to place an object and see if something explodes
|
||||||
def test_smoke_place():
|
def test_smoke_place():
|
||||||
ar = Arrange(30, 30, 15, 15)
|
ar = Arrange(30, 30, 15, 15)
|
||||||
|
@ -80,6 +264,20 @@ def test_checkShape():
|
||||||
assert points3 > points
|
assert points3 > points
|
||||||
|
|
||||||
|
|
||||||
|
## See of our center has less penalty points than out of the center
|
||||||
|
def test_checkShape_rectangular():
|
||||||
|
ar = Arrange(20, 30, 10, 15)
|
||||||
|
ar.centerFirst()
|
||||||
|
print(ar._priority)
|
||||||
|
|
||||||
|
shape_arr = gimmeShapeArray()
|
||||||
|
points = ar.checkShape(0, 0, shape_arr)
|
||||||
|
points2 = ar.checkShape(5, 0, shape_arr)
|
||||||
|
points3 = ar.checkShape(0, 5, shape_arr)
|
||||||
|
assert points2 > points
|
||||||
|
assert points3 > points
|
||||||
|
|
||||||
|
|
||||||
## Check that placing an object on occupied place returns None.
|
## Check that placing an object on occupied place returns None.
|
||||||
def test_checkShape_place():
|
def test_checkShape_place():
|
||||||
ar = Arrange(30, 30, 15, 15)
|
ar = Arrange(30, 30, 15, 15)
|
||||||
|
@ -95,7 +293,7 @@ def test_checkShape_place():
|
||||||
|
|
||||||
## Test the whole sequence
|
## Test the whole sequence
|
||||||
def test_smoke_place_objects():
|
def test_smoke_place_objects():
|
||||||
ar = Arrange(20, 20, 10, 10)
|
ar = Arrange(20, 20, 10, 10, scale = 1)
|
||||||
ar.centerFirst()
|
ar.centerFirst()
|
||||||
shape_arr = gimmeShapeArray()
|
shape_arr = gimmeShapeArray()
|
||||||
|
|
||||||
|
@ -104,6 +302,13 @@ def test_smoke_place_objects():
|
||||||
ar.place(best_spot_x, best_spot_y, shape_arr)
|
ar.place(best_spot_x, best_spot_y, shape_arr)
|
||||||
|
|
||||||
|
|
||||||
|
# Test some internals
|
||||||
|
def test_compare_occupied_and_priority_tables():
|
||||||
|
ar = Arrange(10, 15, 5, 7)
|
||||||
|
ar.centerFirst()
|
||||||
|
assert ar._priority.shape == ar._occupied.shape
|
||||||
|
|
||||||
|
|
||||||
## Polygon -> array
|
## Polygon -> array
|
||||||
def test_arrayFromPolygon():
|
def test_arrayFromPolygon():
|
||||||
vertices = numpy.array([[-3, 1], [3, 1], [0, -3]])
|
vertices = numpy.array([[-3, 1], [3, 1], [0, -3]])
|
||||||
|
@ -145,3 +350,30 @@ def test_check2():
|
||||||
assert numpy.any(check_array)
|
assert numpy.any(check_array)
|
||||||
assert not check_array[3][0]
|
assert not check_array[3][0]
|
||||||
assert check_array[3][4]
|
assert check_array[3][4]
|
||||||
|
|
||||||
|
|
||||||
|
## Just adding some stuff to ensure fromNode works as expected. Some parts should actually be in UM
|
||||||
|
def test_parts_of_fromNode():
|
||||||
|
from UM.Math.Polygon import Polygon
|
||||||
|
p = Polygon(numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32))
|
||||||
|
offset = 1
|
||||||
|
print(p._points)
|
||||||
|
p_offset = p.getMinkowskiHull(Polygon.approximatedCircle(offset))
|
||||||
|
print("--------------")
|
||||||
|
print(p_offset._points)
|
||||||
|
assert len(numpy.where(p_offset._points[:, 0] >= 2.9)) > 0
|
||||||
|
assert len(numpy.where(p_offset._points[:, 0] <= -2.9)) > 0
|
||||||
|
assert len(numpy.where(p_offset._points[:, 1] >= 2.9)) > 0
|
||||||
|
assert len(numpy.where(p_offset._points[:, 1] <= -2.9)) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_parts_of_fromNode2():
|
||||||
|
from UM.Math.Polygon import Polygon
|
||||||
|
p = Polygon(numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32) * 2) # 4x4
|
||||||
|
offset = 13.3
|
||||||
|
scale = 0.5
|
||||||
|
p_offset = p.getMinkowskiHull(Polygon.approximatedCircle(offset))
|
||||||
|
shape_arr1 = ShapeArray.fromPolygon(p._points, scale = scale)
|
||||||
|
shape_arr2 = ShapeArray.fromPolygon(p_offset._points, scale = scale)
|
||||||
|
assert shape_arr1.arr.shape[0] >= (4 * scale) - 1 # -1 is to account for rounding errors
|
||||||
|
assert shape_arr2.arr.shape[0] >= (2 * offset + 4) * scale - 1
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue