CURA-4525 first multi slice + multi layer data, added filter on build plate, added option arrange on load, visuals like convex hull are now correct

This commit is contained in:
Jack Ha 2017-11-09 17:03:20 +01:00
parent 41d5ec86a3
commit e21acd1a07
18 changed files with 468 additions and 260 deletions

View file

@ -40,7 +40,7 @@ class Arrange:
# \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 = 220, y = 220):
arranger = Arrange(x, y, x / 2, y / 2, scale = scale) arranger = Arrange(x, y, x // 2, y // 2, scale = scale)
arranger.centerFirst() arranger.centerFirst()
if fixed_nodes is None: if fixed_nodes is None:

View file

@ -112,7 +112,6 @@ class ArrangeObjectsAllBuildPlatesJob(Job):
# start_priority = 0 # start_priority = 0
while try_placement: while try_placement:
Logger.log("d", "start_priority %s", start_priority)
# 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()

View file

@ -6,7 +6,6 @@ from UM.Scene.SceneNode import SceneNode
from UM.Resources import Resources from UM.Resources import Resources
from UM.Math.Color import Color from UM.Math.Color import Color
from UM.Mesh.MeshBuilder import MeshBuilder # To create a mesh to display the convex hull with. from UM.Mesh.MeshBuilder import MeshBuilder # To create a mesh to display the convex hull with.
from UM.View.GL.OpenGL import OpenGL from UM.View.GL.OpenGL import OpenGL
@ -65,7 +64,7 @@ class ConvexHullNode(SceneNode):
ConvexHullNode.shader.setUniformValue("u_diffuseColor", self._color) ConvexHullNode.shader.setUniformValue("u_diffuseColor", self._color)
ConvexHullNode.shader.setUniformValue("u_opacity", 0.6) ConvexHullNode.shader.setUniformValue("u_opacity", 0.6)
if self.getParent(): if self.getParent() and self.getParent().callDecoration("getBuildPlateNumber") == Application.getInstance().activeBuildPlate:
if self.getMeshData(): if self.getMeshData():
renderer.queueNode(self, transparent = True, shader = ConvexHullNode.shader, backface_cull = True, sort = -8) renderer.queueNode(self, transparent = True, shader = ConvexHullNode.shader, backface_cull = True, sort = -8)
if self._convex_hull_head_mesh: if self._convex_hull_head_mesh:

View file

@ -33,6 +33,7 @@ from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.SetTransformOperation import SetTransformOperation from UM.Operations.SetTransformOperation import SetTransformOperation
from cura.Arrange import Arrange from cura.Arrange import Arrange
from cura.ShapeArray import ShapeArray from cura.ShapeArray import ShapeArray
from cura.ConvexHullDecorator import ConvexHullDecorator from cura.ConvexHullDecorator import ConvexHullDecorator
@ -41,6 +42,7 @@ from cura.SliceableObjectDecorator import SliceableObjectDecorator
from cura.BlockSlicingDecorator import BlockSlicingDecorator from cura.BlockSlicingDecorator import BlockSlicingDecorator
# research # research
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.ArrangeObjectsJob import ArrangeObjectsJob from cura.ArrangeObjectsJob import ArrangeObjectsJob
from cura.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob from cura.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
@ -307,11 +309,13 @@ class CuraApplication(QtApplication):
preferences.addPreference("cura/asked_dialog_on_project_save", False) preferences.addPreference("cura/asked_dialog_on_project_save", False)
preferences.addPreference("cura/choice_on_profile_override", "always_ask") preferences.addPreference("cura/choice_on_profile_override", "always_ask")
preferences.addPreference("cura/choice_on_open_project", "always_ask") preferences.addPreference("cura/choice_on_open_project", "always_ask")
preferences.addPreference("cura/arrange_objects_on_load", True)
preferences.addPreference("cura/currency", "") preferences.addPreference("cura/currency", "")
preferences.addPreference("cura/material_settings", "{}") preferences.addPreference("cura/material_settings", "{}")
preferences.addPreference("view/invert_zoom", False) preferences.addPreference("view/invert_zoom", False)
preferences.addPreference("view/filter_current_build_plate", False)
self._need_to_show_user_agreement = not Preferences.getInstance().getValue("general/accepted_user_agreement") self._need_to_show_user_agreement = not Preferences.getInstance().getValue("general/accepted_user_agreement")
@ -896,7 +900,7 @@ class CuraApplication(QtApplication):
scene_bounding_box = None scene_bounding_box = None
is_block_slicing_node = False is_block_slicing_node = False
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if type(node) is not SceneNode or (not node.getMeshData() and not node.callDecoration("getLayerData")): if not issubclass(type(node), SceneNode) or (not node.getMeshData() and not node.callDecoration("getLayerData")):
continue continue
if node.callDecoration("isBlockSlicing"): if node.callDecoration("isBlockSlicing"):
is_block_slicing_node = True is_block_slicing_node = True
@ -1013,7 +1017,7 @@ class CuraApplication(QtApplication):
Selection.clear() Selection.clear()
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if type(node) is not SceneNode: if not issubclass(type(node), SceneNode):
continue continue
if not node.getMeshData() and not node.callDecoration("isGroup"): if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group. continue # Node that doesnt have a mesh and is not a group.
@ -1021,6 +1025,9 @@ class CuraApplication(QtApplication):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted) continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.isSelectable(): if not node.isSelectable():
continue # i.e. node with layer data continue # i.e. node with layer data
if not node.callDecoration("isSliceable"):
continue # i.e. node with layer data
Selection.add(node) Selection.add(node)
## Delete all nodes containing mesh data in the scene. ## Delete all nodes containing mesh data in the scene.
@ -1032,7 +1039,7 @@ class CuraApplication(QtApplication):
nodes = [] nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if type(node) is not SceneNode: if not issubclass(type(node), SceneNode):
continue continue
if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"): if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group. continue # Node that doesnt have a mesh and is not a group.
@ -1054,7 +1061,7 @@ class CuraApplication(QtApplication):
Logger.log("i", "Resetting all scene translations") Logger.log("i", "Resetting all scene translations")
nodes = [] nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if type(node) is not SceneNode: if not issubclass(type(node), SceneNode):
continue continue
if not node.getMeshData() and not node.callDecoration("isGroup"): if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group. continue # Node that doesnt have a mesh and is not a group.
@ -1082,13 +1089,13 @@ class CuraApplication(QtApplication):
Logger.log("i", "Resetting all scene transformations") Logger.log("i", "Resetting all scene transformations")
nodes = [] nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if type(node) is not SceneNode: if not issubclass(type(node), SceneNode):
continue continue
if not node.getMeshData() and not node.callDecoration("isGroup"): if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group. continue # Node that doesnt have a mesh and is not a group.
if node.getParent() and node.getParent().callDecoration("isGroup"): if node.getParent() and node.getParent().callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted) continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.isSelectable(): if not node.callDecoration("isSliceable"):
continue # i.e. node with layer data continue # i.e. node with layer data
nodes.append(node) nodes.append(node)
@ -1109,7 +1116,27 @@ class CuraApplication(QtApplication):
def arrangeObjectsToAllBuildPlates(self): def arrangeObjectsToAllBuildPlates(self):
nodes = [] nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if type(node) is not SceneNode: if not issubclass(type(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.callDecoration("isSliceable"):
continue # i.e. node with layer data
# Skip nodes that are too big
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
nodes.append(node)
job = ArrangeObjectsAllBuildPlatesJob(nodes)
job.start()
self.setActiveBuildPlate(0)
# Single build plate
@pyqtSlot()
def arrangeAll(self):
nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if not issubclass(type(node), SceneNode):
continue continue
if not node.getMeshData() and not node.callDecoration("isGroup"): if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group. continue # Node that doesnt have a mesh and is not a group.
@ -1117,24 +1144,7 @@ class CuraApplication(QtApplication):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted) continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.isSelectable(): if not node.isSelectable():
continue # i.e. node with layer data continue # i.e. node with layer data
# Skip nodes that are too big if not node.callDecoration("isSliceable"):
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
nodes.append(node)
job = ArrangeObjectsAllBuildPlatesJob(nodes)
job.start()
# Single build plate
@pyqtSlot()
def arrangeAll(self):
nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if type(node) is not 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 continue # i.e. node with layer data
if node.callDecoration("getBuildPlateNumber") == self._active_build_plate: if node.callDecoration("getBuildPlateNumber") == self._active_build_plate:
# Skip nodes that are too big # Skip nodes that are too big
@ -1150,7 +1160,7 @@ class CuraApplication(QtApplication):
# What nodes are on the build plate and are not being moved # What nodes are on the build plate and are not being moved
fixed_nodes = [] fixed_nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if type(node) is not SceneNode: if not issubclass(type(node), SceneNode):
continue continue
if not node.getMeshData() and not node.callDecoration("isGroup"): if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group. continue # Node that doesnt have a mesh and is not a group.
@ -1158,6 +1168,8 @@ class CuraApplication(QtApplication):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted) continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.isSelectable(): if not node.isSelectable():
continue # i.e. node with layer data continue # i.e. node with layer data
if not node.callDecoration("isSliceable"):
continue # i.e. node with layer data
if node in nodes: # exclude selected node from fixed_nodes if node in nodes: # exclude selected node from fixed_nodes
continue continue
fixed_nodes.append(node) fixed_nodes.append(node)
@ -1176,7 +1188,7 @@ class CuraApplication(QtApplication):
Logger.log("i", "Reloading all loaded mesh data.") Logger.log("i", "Reloading all loaded mesh data.")
nodes = [] nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if type(node) is not SceneNode or not node.getMeshData(): if not issubclass(type(node), SceneNode) or not node.getMeshData():
continue continue
nodes.append(node) nodes.append(node)
@ -1267,7 +1279,7 @@ class CuraApplication(QtApplication):
@pyqtSlot() @pyqtSlot()
def groupSelected(self): def groupSelected(self):
# Create a group-node # Create a group-node
group_node = SceneNode() group_node = CuraSceneNode()
group_decorator = GroupDecorator() group_decorator = GroupDecorator()
group_node.addDecorator(group_decorator) group_node.addDecorator(group_decorator)
group_node.addDecorator(ConvexHullDecorator()) group_node.addDecorator(ConvexHullDecorator())
@ -1413,11 +1425,15 @@ class CuraApplication(QtApplication):
min_offset = 8 min_offset = 8
self.fileLoaded.emit(filename) self.fileLoaded.emit(filename)
arrange_objects_on_load = Preferences.getInstance().getValue("cura/arrange_objects_on_load")
target_build_plate = self.activeBuildPlate if arrange_objects_on_load else -1
for original_node in nodes:
node = CuraSceneNode() # We want our own CuraSceneNode
node.setMeshData(original_node.getMeshData())
for node in nodes:
node.setSelectable(True) node.setSelectable(True)
node.setName(os.path.basename(filename)) node.setName(os.path.basename(filename))
node.addDecorator(BuildPlateDecorator())
extension = os.path.splitext(filename)[1] extension = os.path.splitext(filename)[1]
if extension.lower() in self._non_sliceable_extensions: if extension.lower() in self._non_sliceable_extensions:
@ -1442,20 +1458,23 @@ class CuraApplication(QtApplication):
if not child.getDecorator(ConvexHullDecorator): if not child.getDecorator(ConvexHullDecorator):
child.addDecorator(ConvexHullDecorator()) child.addDecorator(ConvexHullDecorator())
if node.callDecoration("isSliceable"): if arrange_objects_on_load:
# Only check position if it's not already blatantly obvious that it won't fit. if node.callDecoration("isSliceable"):
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth: # Only check position if it's not already blatantly obvious that it won't fit.
# Find node location if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset) # Find node location
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset)
# If a model is to small then it will not contain any points # If a model is to small then it will not contain any points
if offset_shape_arr is None and hull_shape_arr is None: if offset_shape_arr is None and hull_shape_arr is None:
Message(self._i18n_catalog.i18nc("@info:status", "The selected model was too small to load."), Message(self._i18n_catalog.i18nc("@info:status", "The selected model was too small to load."),
title=self._i18n_catalog.i18nc("@info:title", "Warning")).show() title=self._i18n_catalog.i18nc("@info:title", "Warning")).show()
return return
# Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher # Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher
node, _ = arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10) node, _ = arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10)
node.addDecorator(BuildPlateDecorator(target_build_plate))
op = AddSceneNodeOperation(node, scene.getRoot()) op = AddSceneNodeOperation(node, scene.getRoot())
op.push() op.push()
@ -1494,6 +1513,8 @@ class CuraApplication(QtApplication):
#### research - hacky place for these kind of thing #### research - hacky place for these kind of thing
@pyqtSlot(int) @pyqtSlot(int)
def setActiveBuildPlate(self, nr): def setActiveBuildPlate(self, nr):
if nr == self._active_build_plate:
return
Logger.log("d", "Select build plate: %s" % nr) Logger.log("d", "Select build plate: %s" % nr)
self._active_build_plate = nr self._active_build_plate = nr

View file

@ -7,23 +7,35 @@ from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection from UM.Scene.Selection import Selection
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
#from cura.Scene.CuraSceneNode import CuraSceneNode
from UM.Preferences import Preferences
class ObjectManager(ListModel): class ObjectManager(ListModel):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._last_selected_index = 0 self._last_selected_index = 0
Application.getInstance().getController().getScene().sceneChanged.connect(self._update) Application.getInstance().getController().getScene().sceneChanged.connect(self._update_scene_changed)
Preferences.getInstance().preferenceChanged.connect(self._update)
Application.getInstance().activeBuildPlateChanged.connect(self._update)
def _update(self, *args): def _update(self, *args):
nodes = [] nodes = []
filter_current_build_plate = Preferences.getInstance().getValue("view/filter_current_build_plate")
active_build_plate_number = Application.getInstance().activeBuildPlate
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
if type(node) is not SceneNode or (not node.getMeshData() and not node.callDecoration("getLayerData")): if not issubclass(type(node), SceneNode) or (not node.getMeshData() and not node.callDecoration("getLayerData")):
continue
if not node.callDecoration("isSliceable"):
continue
node_build_plate_number = node.callDecoration("getBuildPlateNumber")
if filter_current_build_plate and node_build_plate_number != active_build_plate_number:
continue continue
nodes.append({ nodes.append({
"name": node.getName(), "name": node.getName(),
"isSelected": Selection.isSelected(node), "isSelected": Selection.isSelected(node),
"buildPlateNumber": node.callDecoration("getBuildPlateNumber"), "isOutsideBuildArea": node.isOutsideBuildArea(),
"buildPlateNumber": node_build_plate_number,
"node": node "node": node
}) })
nodes = sorted(nodes, key=lambda n: n["name"]) nodes = sorted(nodes, key=lambda n: n["name"])
@ -31,6 +43,12 @@ class ObjectManager(ListModel):
self.itemsChanged.emit() self.itemsChanged.emit()
def _update_scene_changed(self, *args):
# if args and type(args[0]) is not CuraSceneNode:
# Logger.log("d", " ascdf %s", args)
# return
self._update(*args)
## Either select or deselect an item ## Either select or deselect an item
@pyqtSlot(int) @pyqtSlot(int)
def changeSelection(self, index): def changeSelection(self, index):
@ -63,6 +81,11 @@ class ObjectManager(ListModel):
self._last_selected_index = index self._last_selected_index = index
# testing
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
if node.callDecoration("getLayerData"):
Logger.log("d", " ##### NODE: %s", node)
@staticmethod @staticmethod
def createObjectManager(): def createObjectManager():
return ObjectManager() return ObjectManager()

View file

@ -6,11 +6,14 @@ from UM.Logger import Logger
class BuildPlateDecorator(SceneNodeDecorator): class BuildPlateDecorator(SceneNodeDecorator):
def __init__(self, build_plate_number = -1): def __init__(self, build_plate_number = -1):
super().__init__() super().__init__()
self._build_plate_number = None
self._previous_build_plate_number = None
self.setBuildPlateNumber(build_plate_number) self.setBuildPlateNumber(build_plate_number)
def setBuildPlateNumber(self, nr): def setBuildPlateNumber(self, nr):
# Make sure that groups are set correctly # Make sure that groups are set correctly
# setBuildPlateForSelection in CuraActions makes sure that no single childs are set. # setBuildPlateForSelection in CuraActions makes sure that no single childs are set.
self._previous_build_plate_number = self._build_plate_number
self._build_plate_number = nr self._build_plate_number = nr
if self._node and self._node.callDecoration("isGroup"): if self._node and self._node.callDecoration("isGroup"):
for child in self._node.getChildren(): for child in self._node.getChildren():
@ -19,5 +22,9 @@ class BuildPlateDecorator(SceneNodeDecorator):
def getBuildPlateNumber(self): def getBuildPlateNumber(self):
return self._build_plate_number return self._build_plate_number
# Used to determine from what build plate the node moved.
def getPreviousBuildPlateNumber(self):
return self._previous_build_plate_number
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
return BuildPlateDecorator() return BuildPlateDecorator()

View file

@ -0,0 +1,40 @@
from UM.Application import Application
from UM.Logger import Logger
from UM.Scene.SceneNode import SceneNode
from copy import deepcopy
## Scene nodes that are models are only seen when selecting the corresponding build plate
# Note that many other nodes can just be UM SceneNode objects.
class CuraSceneNode(SceneNode):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._outside_buildarea = True
def setOutsideBuildArea(self, new_value):
self._outside_buildarea = new_value
def isOutsideBuildArea(self):
return self._outside_buildarea or self.callDecoration("getBuildPlateNumber") < 0
def isVisible(self):
return super().isVisible() and self.callDecoration("getBuildPlateNumber") == Application.getInstance().activeBuildPlate
def isSelectable(self) -> bool:
return super().isSelectable() and self.callDecoration("getBuildPlateNumber") == Application.getInstance().activeBuildPlate
## Taken from SceneNode, but replaced SceneNode with CuraSceneNode
def __deepcopy__(self, memo):
copy = CuraSceneNode()
copy.setTransformation(self.getLocalTransformation())
copy.setMeshData(self._mesh_data)
copy.setVisible(deepcopy(self._visible, memo))
copy._selectable = deepcopy(self._selectable, memo)
copy._name = deepcopy(self._name, memo)
for decorator in self._decorators:
copy.addDecorator(deepcopy(decorator, memo))
for child in self._children:
copy.addChild(deepcopy(child, memo))
self.calculateBoundingBoxMesh()
return copy

View file

@ -15,7 +15,8 @@ from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
from UM.Application import Application from UM.Application import Application
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from cura.QualityManager import QualityManager from cura.QualityManager import QualityManager
from UM.Scene.SceneNode import SceneNode #from UM.Scene.SceneNode import SceneNode
from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode
from cura.SliceableObjectDecorator import SliceableObjectDecorator from cura.SliceableObjectDecorator import SliceableObjectDecorator
from cura.ZOffsetDecorator import ZOffsetDecorator from cura.ZOffsetDecorator import ZOffsetDecorator

View file

@ -69,9 +69,10 @@ class CuraEngineBackend(QObject, Backend):
# Workaround to disable layer view processing if layer view is not active. # Workaround to disable layer view processing if layer view is not active.
self._layer_view_active = False self._layer_view_active = False
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
Application.getInstance().activeBuildPlateChanged.connect(self._onActiveViewChanged)
self._onActiveViewChanged() self._onActiveViewChanged()
self._stored_layer_data = [] self._stored_layer_data = []
self._stored_optimized_layer_data = [] self._stored_optimized_layer_data = {} # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
self._scene = Application.getInstance().getController().getScene() self._scene = Application.getInstance().getController().getScene()
self._scene.sceneChanged.connect(self._onSceneChanged) self._scene.sceneChanged.connect(self._onSceneChanged)
@ -104,12 +105,14 @@ class CuraEngineBackend(QObject, Backend):
self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage
self._start_slice_job = None self._start_slice_job = None
self._start_slice_job_build_plate = None
self._slicing = False # Are we currently slicing? self._slicing = False # Are we currently slicing?
self._restart = False # Back-end is currently restarting? self._restart = False # Back-end is currently restarting?
self._tool_active = False # If a tool is active, some tasks do not have to do anything self._tool_active = False # If a tool is active, some tasks do not have to do anything
self._always_restart = True # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness. self._always_restart = True # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness.
self._process_layers_job = None # The currently active job to process layers, or None if it is not processing layers. self._process_layers_job = None # The currently active job to process layers, or None if it is not processing layers.
self._need_slicing = False # self._need_slicing = False
self._build_plates_to_be_sliced = [] # what needs slicing?
self._engine_is_fresh = True # Is the newly started engine used before or not? self._engine_is_fresh = True # Is the newly started engine used before or not?
self._backend_log_max_lines = 20000 # Maximum number of lines to buffer self._backend_log_max_lines = 20000 # Maximum number of lines to buffer
@ -189,8 +192,9 @@ class CuraEngineBackend(QObject, Backend):
## Perform a slice of the scene. ## Perform a slice of the scene.
def slice(self): def slice(self):
Logger.log("d", "starting to slice again!")
self._slice_start_time = time() self._slice_start_time = time()
if not self._need_slicing: if not self._build_plates_to_be_sliced:
self.processingProgress.emit(1.0) self.processingProgress.emit(1.0)
self.backendStateChange.emit(BackendState.Done) self.backendStateChange.emit(BackendState.Done)
Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.") Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.")
@ -199,7 +203,6 @@ class CuraEngineBackend(QObject, Backend):
Application.getInstance().getPrintInformation().setToZeroPrintInformation() Application.getInstance().getPrintInformation().setToZeroPrintInformation()
self._stored_layer_data = [] self._stored_layer_data = []
self._stored_optimized_layer_data = []
if self._process is None: if self._process is None:
self._createSocket() self._createSocket()
@ -215,6 +218,9 @@ class CuraEngineBackend(QObject, Backend):
slice_message = self._socket.createMessage("cura.proto.Slice") slice_message = self._socket.createMessage("cura.proto.Slice")
self._start_slice_job = StartSliceJob.StartSliceJob(slice_message) self._start_slice_job = StartSliceJob.StartSliceJob(slice_message)
self._start_slice_job_build_plate = self._build_plates_to_be_sliced.pop(0)
self._stored_optimized_layer_data[self._start_slice_job_build_plate] = []
self._start_slice_job.setBuildPlate(self._start_slice_job_build_plate)
self._start_slice_job.start() self._start_slice_job.start()
self._start_slice_job.finished.connect(self._onStartSliceCompleted) self._start_slice_job.finished.connect(self._onStartSliceCompleted)
@ -223,7 +229,8 @@ class CuraEngineBackend(QObject, Backend):
def _terminate(self): def _terminate(self):
self._slicing = False self._slicing = False
self._stored_layer_data = [] self._stored_layer_data = []
self._stored_optimized_layer_data = [] if self._start_slice_job_build_plate in self._stored_optimized_layer_data:
del self._stored_optimized_layer_data[self._start_slice_job_build_plate]
if self._start_slice_job is not None: if self._start_slice_job is not None:
self._start_slice_job.cancel() self._start_slice_job.cancel()
@ -315,10 +322,13 @@ class CuraEngineBackend(QObject, Backend):
self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."), self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."),
title = catalog.i18nc("@info:title", "Unable to slice")) title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show() self._error_message.show()
self.backendStateChange.emit(BackendState.Error) #self.backendStateChange.emit(BackendState.Error)
else: else:
self.backendStateChange.emit(BackendState.NotStarted) #self.backendStateChange.emit(BackendState.NotStarted)
pass
self._invokeSlice()
return return
# Preparation completed, send it to the backend. # Preparation completed, send it to the backend.
self._socket.sendMessage(job.getSliceMessage()) self._socket.sendMessage(job.getSliceMessage())
@ -360,27 +370,34 @@ class CuraEngineBackend(QObject, Backend):
# #
# \param source The scene node that was changed. # \param source The scene node that was changed.
def _onSceneChanged(self, source): def _onSceneChanged(self, source):
if type(source) is not SceneNode: Logger.log("d", " ##### scene changed: %s", source)
if not issubclass(type(source), SceneNode):
return return
root_scene_nodes_changed = False root_scene_nodes_changed = False
build_plates_changed = set()
if source == self._scene.getRoot(): if source == self._scene.getRoot():
num_objects = 0 num_objects = 0
for node in DepthFirstIterator(self._scene.getRoot()): for node in DepthFirstIterator(self._scene.getRoot()):
# Only count sliceable objects # Only count sliceable objects
if node.callDecoration("isSliceable"): if node.callDecoration("isSliceable"):
num_objects += 1 num_objects += 1
build_plates_changed.add(node.callDecoration("getBuildPlateNumber"))
build_plates_changed.add(node.callDecoration("getPreviousBuildPlateNumber"))
if num_objects != self._last_num_objects: if num_objects != self._last_num_objects:
self._last_num_objects = num_objects self._last_num_objects = num_objects
root_scene_nodes_changed = True root_scene_nodes_changed = True
else: # else:
return # return # ??
build_plates_changed.discard(None)
build_plates_changed.discard(-1) # object not on build plate
Logger.log("d", " #### build plates changed: %s", build_plates_changed)
if not source.callDecoration("isGroup") and not root_scene_nodes_changed: # if not source.callDecoration("isGroup") and not root_scene_nodes_changed:
if source.getMeshData() is None: # if source.getMeshData() is None:
return # return
if source.getMeshData().getVertices() is None: # if source.getMeshData().getVertices() is None:
return # return
if self._tool_active: if self._tool_active:
# do it later, each source only has to be done once # do it later, each source only has to be done once
@ -388,9 +405,24 @@ class CuraEngineBackend(QObject, Backend):
self._postponed_scene_change_sources.append(source) self._postponed_scene_change_sources.append(source)
return return
self.needsSlicing() if build_plates_changed:
self.stopSlicing() Logger.log("d", " going to reslice")
self._onChanged() self.stopSlicing()
for build_plate_number in build_plates_changed:
if build_plate_number not in self._build_plates_to_be_sliced:
self._build_plates_to_be_sliced.append(build_plate_number)
self.processingProgress.emit(0.0)
self.backendStateChange.emit(BackendState.NotStarted)
if not self._use_timer:
# With manually having to slice, we want to clear the old invalid layer data.
self._clearLayerData(build_plates_changed)
self._invokeSlice()
# #self.needsSlicing()
# self.stopSlicing()
# #self._onChanged()
# self._invokeSlice()
## Called when an error occurs in the socket connection towards the engine. ## Called when an error occurs in the socket connection towards the engine.
# #
@ -410,16 +442,24 @@ class CuraEngineBackend(QObject, Backend):
Logger.log("w", "A socket error caused the connection to be reset") Logger.log("w", "A socket error caused the connection to be reset")
## Remove old layer data (if any) ## Remove old layer data (if any)
def _clearLayerData(self): def _clearLayerData(self, build_plate_numbers = set()):
for node in DepthFirstIterator(self._scene.getRoot()): for node in DepthFirstIterator(self._scene.getRoot()):
if node.callDecoration("getLayerData"): if node.callDecoration("getLayerData"):
node.getParent().removeChild(node) if node.callDecoration("getBuildPlateNumber") in build_plate_numbers or not build_plate_numbers:
break node.getParent().removeChild(node)
## Convenient function: set need_slicing, emit state and clear layer data def markSliceAll(self):
if 0 not in self._build_plates_to_be_sliced:
self._build_plates_to_be_sliced.append(0)
if 1 not in self._build_plates_to_be_sliced:
self._build_plates_to_be_sliced.append(1)
if 2 not in self._build_plates_to_be_sliced:
self._build_plates_to_be_sliced.append(2)
## Convenient function: mark everything to slice, emit state and clear layer data
def needsSlicing(self): def needsSlicing(self):
self.stopSlicing() self.stopSlicing()
self._need_slicing = True self.markSliceAll()
self.processingProgress.emit(0.0) self.processingProgress.emit(0.0)
self.backendStateChange.emit(BackendState.NotStarted) self.backendStateChange.emit(BackendState.NotStarted)
if not self._use_timer: if not self._use_timer:
@ -441,7 +481,7 @@ class CuraEngineBackend(QObject, Backend):
def _onStackErrorCheckFinished(self): def _onStackErrorCheckFinished(self):
self._is_error_check_scheduled = False self._is_error_check_scheduled = False
if not self._slicing and self._need_slicing: if not self._slicing and self._build_plates_to_be_sliced: #self._need_slicing:
self.needsSlicing() self.needsSlicing()
self._onChanged() self._onChanged()
@ -455,7 +495,7 @@ class CuraEngineBackend(QObject, Backend):
# #
# \param message The protobuf message containing sliced layer data. # \param message The protobuf message containing sliced layer data.
def _onOptimizedLayerMessage(self, message): def _onOptimizedLayerMessage(self, message):
self._stored_optimized_layer_data.append(message) self._stored_optimized_layer_data[self._start_slice_job_build_plate].append(message)
## Called when a progress message is received from the engine. ## Called when a progress message is received from the engine.
# #
@ -464,6 +504,16 @@ class CuraEngineBackend(QObject, Backend):
self.processingProgress.emit(message.amount) self.processingProgress.emit(message.amount)
self.backendStateChange.emit(BackendState.Processing) self.backendStateChange.emit(BackendState.Processing)
# testing
def _invokeSlice(self):
if self._use_timer:
# if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
# otherwise business as usual
if self._is_error_check_scheduled:
self._change_timer.stop()
else:
self._change_timer.start()
## Called when the engine sends a message that slicing is finished. ## Called when the engine sends a message that slicing is finished.
# #
# \param message The protobuf message signalling that slicing is finished. # \param message The protobuf message signalling that slicing is finished.
@ -481,13 +531,20 @@ class CuraEngineBackend(QObject, Backend):
self._scene.gcode_list[self._scene.gcode_list.index(line)] = replaced self._scene.gcode_list[self._scene.gcode_list.index(line)] = replaced
self._slicing = False self._slicing = False
self._need_slicing = False #self._need_slicing = False
Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time ) Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time )
if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()):
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data) # See if we need to process the sliced layers job.
self._process_layers_job.finished.connect(self._onProcessLayersFinished) active_build_plate = Application.getInstance().activeBuildPlate
self._process_layers_job.start() if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()) and active_build_plate == self._start_slice_job_build_plate:
self._stored_optimized_layer_data = [] self._startProcessSlicedLayersJob(active_build_plate)
self._start_slice_job_build_plate = None
Logger.log("d", "See if there is more to slice...")
# Somehow this results in an Arcus Error
# self.slice()
# Testing call slice again, allow backend to restart by using the timer
self._invokeSlice()
## Called when a g-code message is received from the engine. ## Called when a g-code message is received from the engine.
# #
@ -584,19 +641,26 @@ class CuraEngineBackend(QObject, Backend):
source = self._postponed_scene_change_sources.pop(0) source = self._postponed_scene_change_sources.pop(0)
self._onSceneChanged(source) self._onSceneChanged(source)
def _startProcessSlicedLayersJob(self, build_plate_number):
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data[build_plate_number])
self._process_layers_job.setBuildPlate(build_plate_number)
self._process_layers_job.finished.connect(self._onProcessLayersFinished)
self._process_layers_job.start()
del self._stored_optimized_layer_data[build_plate_number]
## Called when the user changes the active view mode. ## Called when the user changes the active view mode.
def _onActiveViewChanged(self): def _onActiveViewChanged(self):
if Application.getInstance().getController().getActiveView(): application = Application.getInstance()
view = Application.getInstance().getController().getActiveView() view = application.getController().getActiveView()
if view:
active_build_plate = application.activeBuildPlate
if view.getPluginId() == "LayerView": # If switching to layer view, we should process the layers if that hasn't been done yet. if view.getPluginId() == "LayerView": # If switching to layer view, we should process the layers if that hasn't been done yet.
self._layer_view_active = True self._layer_view_active = True
# There is data and we're not slicing at the moment # There is data and we're not slicing at the moment
# if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment. # if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment.
if self._stored_optimized_layer_data and not self._slicing: # TODO: what build plate I am slicing
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data) if active_build_plate in self._stored_optimized_layer_data and not self._slicing:
self._process_layers_job.finished.connect(self._onProcessLayersFinished) self._startProcessSlicedLayersJob(active_build_plate)
self._process_layers_job.start()
self._stored_optimized_layer_data = []
else: else:
self._layer_view_active = False self._layer_view_active = False

View file

@ -17,10 +17,12 @@ from UM.Logger import Logger
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from cura import LayerDataBuilder from cura import LayerDataBuilder
from cura import LayerDataDecorator from cura import LayerDataDecorator
from cura import LayerPolygon from cura import LayerPolygon
# from cura.Scene.CuraSceneNode import CuraSceneNode
import numpy import numpy
from time import time from time import time
@ -49,6 +51,7 @@ class ProcessSlicedLayersJob(Job):
self._scene = Application.getInstance().getController().getScene() self._scene = Application.getInstance().getController().getScene()
self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1)
self._abort_requested = False self._abort_requested = False
self._build_plate_number = None
## Aborts the processing of layers. ## Aborts the processing of layers.
# #
@ -59,7 +62,11 @@ class ProcessSlicedLayersJob(Job):
def abort(self): def abort(self):
self._abort_requested = True self._abort_requested = True
def setBuildPlate(self, new_value):
self._build_plate_number = new_value
def run(self): def run(self):
Logger.log("d", "########## Processing new layer for [%s]..." % self._build_plate_number)
start_time = time() start_time = time()
if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView":
self._progress_message.show() self._progress_message.show()
@ -72,16 +79,18 @@ class ProcessSlicedLayersJob(Job):
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
new_node = SceneNode() new_node = SceneNode()
new_node.addDecorator(BuildPlateDecorator(self._build_plate_number))
## Remove old layer data (if any) # ## Remove old layer data (if any)
for node in DepthFirstIterator(self._scene.getRoot()): # for node in DepthFirstIterator(self._scene.getRoot()):
if node.callDecoration("getLayerData"): # if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number:
node.getParent().removeChild(node) # Logger.log("d", " # Removing: %s", node)
break # node.getParent().removeChild(node)
if self._abort_requested: # #break
if self._progress_message: # if self._abort_requested:
self._progress_message.hide() # if self._progress_message:
return # self._progress_message.hide()
# return
# Force garbage collection. # Force garbage collection.
# For some reason, Python has a tendency to keep the layer data # For some reason, Python has a tendency to keep the layer data

View file

@ -10,12 +10,13 @@ from UM.Job import Job
from UM.Application import Application from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from UM.Scene.SceneNode import SceneNode #from UM.Scene.SceneNode import SceneNode
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Settings.Validator import ValidatorState from UM.Settings.Validator import ValidatorState
from UM.Settings.SettingRelation import RelationType from UM.Settings.SettingRelation import RelationType
from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode
from cura.OneAtATimeIterator import OneAtATimeIterator from cura.OneAtATimeIterator import OneAtATimeIterator
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
@ -58,10 +59,14 @@ class StartSliceJob(Job):
self._scene = Application.getInstance().getController().getScene() self._scene = Application.getInstance().getController().getScene()
self._slice_message = slice_message self._slice_message = slice_message
self._is_cancelled = False self._is_cancelled = False
self._build_plate_number = None
def getSliceMessage(self): def getSliceMessage(self):
return self._slice_message return self._slice_message
def setBuildPlate(self, build_plate_number):
self._build_plate_number = build_plate_number
## Check if a stack has any errors. ## Check if a stack has any errors.
## returns true if it has errors, false otherwise. ## returns true if it has errors, false otherwise.
def _checkStackForErrors(self, stack): def _checkStackForErrors(self, stack):
@ -78,6 +83,10 @@ class StartSliceJob(Job):
## Runs the job that initiates the slicing. ## Runs the job that initiates the slicing.
def run(self): def run(self):
if self._build_plate_number is None:
self.setResult(StartJobResult.Error)
return
stack = Application.getInstance().getGlobalContainerStack() stack = Application.getInstance().getGlobalContainerStack()
if not stack: if not stack:
self.setResult(StartJobResult.Error) self.setResult(StartJobResult.Error)
@ -141,14 +150,12 @@ class StartSliceJob(Job):
for node in DepthFirstIterator(self._scene.getRoot()): for node in DepthFirstIterator(self._scene.getRoot()):
if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None: if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
# temp hack to filter on build plate 0 if (node.callDecoration("getBuildPlateNumber") == self._build_plate_number):
if (node.callDecoration("getBuildPlateNumber") == 0): if not getattr(node, "_outside_buildarea", False) or (node.callDecoration("getStack") and any(node.callDecoration("getStack").getProperty(setting, "value") for setting in self._not_printed_mesh_settings)):
if not getattr(node, "_outside_buildarea", False)\
or (node.callDecoration("getStack") and any(node.callDecoration("getStack").getProperty(setting, "value") for setting in self._not_printed_mesh_settings)):
temp_list.append(node) temp_list.append(node)
Job.yieldThread() Job.yieldThread()
Logger.log("d", " objects to be sliced: %s", temp_list)
if temp_list: if temp_list:
object_groups.append(temp_list) object_groups.append(temp_list)

View file

@ -8,12 +8,14 @@ from PyQt5.QtCore import Qt
from UM.Mesh.MeshReader import MeshReader from UM.Mesh.MeshReader import MeshReader
from UM.Mesh.MeshBuilder import MeshBuilder from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Scene.SceneNode import SceneNode #from UM.Scene.SceneNode import SceneNode
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from UM.Job import Job from UM.Job import Job
from UM.Logger import Logger from UM.Logger import Logger
from .ImageReaderUI import ImageReaderUI from .ImageReaderUI import ImageReaderUI
from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode
class ImageReader(MeshReader): class ImageReader(MeshReader):
def __init__(self): def __init__(self):

View file

@ -66,13 +66,14 @@ class LayerPass(RenderPass):
self.bind() self.bind()
tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Overlay) tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Overlay)
active_build_plate = Application.getInstance().activeBuildPlate
for node in DepthFirstIterator(self._scene.getRoot()): for node in DepthFirstIterator(self._scene.getRoot()):
if isinstance(node, ToolHandle): if isinstance(node, ToolHandle):
tool_handle_batch.addItem(node.getWorldTransformation(), mesh = node.getSolidMesh()) tool_handle_batch.addItem(node.getWorldTransformation(), mesh = node.getSolidMesh())
elif isinstance(node, SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible(): elif issubclass(type(node), SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible() and node.callDecoration("getBuildPlateNumber") == active_build_plate:
layer_data = node.callDecoration("getLayerData") layer_data = node.callDecoration("getLayerData")
if not layer_data: if not layer_data:
continue continue

View file

@ -75,7 +75,7 @@ class SolidView(View):
for node in DepthFirstIterator(scene.getRoot()): for node in DepthFirstIterator(scene.getRoot()):
if not node.render(renderer): if not node.render(renderer):
if node.getMeshData() and node.isVisible() and (node.callDecoration("getBuildPlateNumber") == activeBuildPlateNumber): if node.getMeshData() and node.isVisible(): # and (node.callDecoration("getBuildPlateNumber") == activeBuildPlateNumber):
uniforms = {} uniforms = {}
shade_factor = 1.0 shade_factor = 1.0

View file

@ -11,7 +11,8 @@ from UM.Math.Matrix import Matrix
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from UM.Mesh.MeshBuilder import MeshBuilder from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Mesh.MeshReader import MeshReader from UM.Mesh.MeshReader import MeshReader
from UM.Scene.SceneNode import SceneNode #from UM.Scene.SceneNode import SceneNode
from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode
MYPY = False MYPY = False
try: try:
@ -19,63 +20,63 @@ try:
import xml.etree.cElementTree as ET import xml.etree.cElementTree as ET
except ImportError: except ImportError:
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
# TODO: preserve the structure of scenes that contain several objects # TODO: preserve the structure of scenes that contain several objects
# Use CADPart, for example, to distinguish between separate objects # Use CADPart, for example, to distinguish between separate objects
DEFAULT_SUBDIV = 16 # Default subdivision factor for spheres, cones, and cylinders DEFAULT_SUBDIV = 16 # Default subdivision factor for spheres, cones, and cylinders
EPSILON = 0.000001 EPSILON = 0.000001
class Shape: class Shape:
# Expects verts in MeshBuilder-ready format, as a n by 3 mdarray # Expects verts in MeshBuilder-ready format, as a n by 3 mdarray
# with vertices stored in rows # with vertices stored in rows
def __init__(self, verts, faces, index_base, name): def __init__(self, verts, faces, index_base, name):
self.verts = verts self.verts = verts
self.faces = faces self.faces = faces
# Those are here for debugging purposes only # Those are here for debugging purposes only
self.index_base = index_base self.index_base = index_base
self.name = name self.name = name
class X3DReader(MeshReader): class X3DReader(MeshReader):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._supported_extensions = [".x3d"] self._supported_extensions = [".x3d"]
self._namespaces = {} self._namespaces = {}
# Main entry point # Main entry point
# Reads the file, returns a SceneNode (possibly with nested ones), or None # Reads the file, returns a SceneNode (possibly with nested ones), or None
def read(self, file_name): def read(self, file_name):
try: try:
self.defs = {} self.defs = {}
self.shapes = [] self.shapes = []
tree = ET.parse(file_name) tree = ET.parse(file_name)
xml_root = tree.getroot() xml_root = tree.getroot()
if xml_root.tag != "X3D": if xml_root.tag != "X3D":
return None return None
scale = 1000 # Default X3D unit it one meter, while Cura's is one millimeters scale = 1000 # Default X3D unit it one meter, while Cura's is one millimeters
if xml_root[0].tag == "head": if xml_root[0].tag == "head":
for head_node in xml_root[0]: for head_node in xml_root[0]:
if head_node.tag == "unit" and head_node.attrib.get("category") == "length": if head_node.tag == "unit" and head_node.attrib.get("category") == "length":
scale *= float(head_node.attrib["conversionFactor"]) scale *= float(head_node.attrib["conversionFactor"])
break break
xml_scene = xml_root[1] xml_scene = xml_root[1]
else: else:
xml_scene = xml_root[0] xml_scene = xml_root[0]
if xml_scene.tag != "Scene": if xml_scene.tag != "Scene":
return None return None
self.transform = Matrix() self.transform = Matrix()
self.transform.setByScaleFactor(scale) self.transform.setByScaleFactor(scale)
self.index_base = 0 self.index_base = 0
# Traverse the scene tree, populate the shapes list # Traverse the scene tree, populate the shapes list
self.processChildNodes(xml_scene) self.processChildNodes(xml_scene)
if self.shapes: if self.shapes:
builder = MeshBuilder() builder = MeshBuilder()
builder.setVertices(numpy.concatenate([shape.verts for shape in self.shapes])) builder.setVertices(numpy.concatenate([shape.verts for shape in self.shapes]))
@ -95,20 +96,20 @@ class X3DReader(MeshReader):
else: else:
return None return None
except Exception: except Exception:
Logger.logException("e", "Exception in X3D reader") Logger.logException("e", "Exception in X3D reader")
return None return None
return node return node
# ------------------------- XML tree traversal # ------------------------- XML tree traversal
def processNode(self, xml_node): def processNode(self, xml_node):
xml_node = self.resolveDefUse(xml_node) xml_node = self.resolveDefUse(xml_node)
if xml_node is None: if xml_node is None:
return return
tag = xml_node.tag tag = xml_node.tag
if tag in ("Group", "StaticGroup", "CADAssembly", "CADFace", "CADLayer", "Collision"): if tag in ("Group", "StaticGroup", "CADAssembly", "CADFace", "CADLayer", "Collision"):
self.processChildNodes(xml_node) self.processChildNodes(xml_node)
@ -120,8 +121,8 @@ class X3DReader(MeshReader):
self.processTransform(xml_node) self.processTransform(xml_node)
elif tag == "Shape": elif tag == "Shape":
self.processShape(xml_node) self.processShape(xml_node)
def processShape(self, xml_node): def processShape(self, xml_node):
# Find the geometry and the appearance inside the Shape # Find the geometry and the appearance inside the Shape
geometry = appearance = None geometry = appearance = None
@ -130,21 +131,21 @@ class X3DReader(MeshReader):
appearance = self.resolveDefUse(sub_node) appearance = self.resolveDefUse(sub_node)
elif sub_node.tag in self.geometry_importers and not geometry: elif sub_node.tag in self.geometry_importers and not geometry:
geometry = self.resolveDefUse(sub_node) geometry = self.resolveDefUse(sub_node)
# TODO: appearance is completely ignored. At least apply the material color... # TODO: appearance is completely ignored. At least apply the material color...
if not geometry is None: if not geometry is None:
try: try:
self.verts = self.faces = [] # Safeguard self.verts = self.faces = [] # Safeguard
self.geometry_importers[geometry.tag](self, geometry) self.geometry_importers[geometry.tag](self, geometry)
m = self.transform.getData() m = self.transform.getData()
verts = m.dot(self.verts)[:3].transpose() verts = m.dot(self.verts)[:3].transpose()
self.shapes.append(Shape(verts, self.faces, self.index_base, geometry.tag)) self.shapes.append(Shape(verts, self.faces, self.index_base, geometry.tag))
self.index_base += len(verts) self.index_base += len(verts)
except Exception: except Exception:
Logger.logException("e", "Exception in X3D reader while reading %s", geometry.tag) Logger.logException("e", "Exception in X3D reader while reading %s", geometry.tag)
# Returns the referenced node if the node has USE, the same node otherwise. # Returns the referenced node if the node has USE, the same node otherwise.
# May return None is USE points at a nonexistent node # May return None is USE points at a nonexistent node
# In X3DOM, when both DEF and USE are in the same node, DEF is ignored. # In X3DOM, when both DEF and USE are in the same node, DEF is ignored.
@ -155,34 +156,34 @@ class X3DReader(MeshReader):
if USE: if USE:
return self.defs.get(USE, None) return self.defs.get(USE, None)
DEF = node.attrib.get("DEF") DEF = node.attrib.get("DEF")
if DEF: if DEF:
self.defs[DEF] = node self.defs[DEF] = node
return node return node
def processChildNodes(self, node): def processChildNodes(self, node):
for c in node: for c in node:
self.processNode(c) self.processNode(c)
Job.yieldThread() Job.yieldThread()
# Since this is a grouping node, will recurse down the tree. # Since this is a grouping node, will recurse down the tree.
# According to the spec, the final transform matrix is: # According to the spec, the final transform matrix is:
# T * C * R * SR * S * -SR * -C # T * C * R * SR * S * -SR * -C
# Where SR corresponds to the rotation matrix to scaleOrientation # Where SR corresponds to the rotation matrix to scaleOrientation
# C and SR are rather exotic. S, slightly less so. # C and SR are rather exotic. S, slightly less so.
def processTransform(self, node): def processTransform(self, node):
rot = readRotation(node, "rotation", (0, 0, 1, 0)) # (angle, axisVactor) tuple rot = readRotation(node, "rotation", (0, 0, 1, 0)) # (angle, axisVactor) tuple
trans = readVector(node, "translation", (0, 0, 0)) # Vector trans = readVector(node, "translation", (0, 0, 0)) # Vector
scale = readVector(node, "scale", (1, 1, 1)) # Vector scale = readVector(node, "scale", (1, 1, 1)) # Vector
center = readVector(node, "center", (0, 0, 0)) # Vector center = readVector(node, "center", (0, 0, 0)) # Vector
scale_orient = readRotation(node, "scaleOrientation", (0, 0, 1, 0)) # (angle, axisVactor) tuple scale_orient = readRotation(node, "scaleOrientation", (0, 0, 1, 0)) # (angle, axisVactor) tuple
# Store the previous transform; in Cura, the default matrix multiplication is in place # Store the previous transform; in Cura, the default matrix multiplication is in place
prev = Matrix(self.transform.getData()) # It's deep copy, I've checked prev = Matrix(self.transform.getData()) # It's deep copy, I've checked
# The rest of transform manipulation will be applied in place # The rest of transform manipulation will be applied in place
got_center = (center.x != 0 or center.y != 0 or center.z != 0) got_center = (center.x != 0 or center.y != 0 or center.z != 0)
T = self.transform T = self.transform
if trans.x != 0 or trans.y != 0 or trans.z !=0: if trans.x != 0 or trans.y != 0 or trans.z !=0:
T.translate(trans) T.translate(trans)
@ -202,13 +203,13 @@ class X3DReader(MeshReader):
T.rotateByAxis(-scale_orient[0], scale_orient[1]) T.rotateByAxis(-scale_orient[0], scale_orient[1])
if got_center: if got_center:
T.translate(-center) T.translate(-center)
self.processChildNodes(node) self.processChildNodes(node)
self.transform = prev self.transform = prev
# ------------------------- Geometry importers # ------------------------- Geometry importers
# They are supposed to fill the self.verts and self.faces arrays, the caller will do the rest # They are supposed to fill the self.verts and self.faces arrays, the caller will do the rest
# Primitives # Primitives
def processGeometryBox(self, node): def processGeometryBox(self, node):
@ -228,14 +229,14 @@ class X3DReader(MeshReader):
self.addVertex(-dx, -dy, dz) self.addVertex(-dx, -dy, dz)
self.addVertex(-dx, -dy, -dz) self.addVertex(-dx, -dy, -dz)
self.addVertex(dx, -dy, -dz) self.addVertex(dx, -dy, -dz)
self.addQuad(0, 1, 2, 3) # +y self.addQuad(0, 1, 2, 3) # +y
self.addQuad(4, 0, 3, 7) # +x self.addQuad(4, 0, 3, 7) # +x
self.addQuad(7, 3, 2, 6) # -z self.addQuad(7, 3, 2, 6) # -z
self.addQuad(6, 2, 1, 5) # -x self.addQuad(6, 2, 1, 5) # -x
self.addQuad(5, 1, 0, 4) # +z self.addQuad(5, 1, 0, 4) # +z
self.addQuad(7, 6, 5, 4) # -y self.addQuad(7, 6, 5, 4) # -y
# The sphere is subdivided into nr rings and ns segments # The sphere is subdivided into nr rings and ns segments
def processGeometrySphere(self, node): def processGeometrySphere(self, node):
r = readFloat(node, "radius", 0.5) r = readFloat(node, "radius", 0.5)
@ -247,16 +248,16 @@ class X3DReader(MeshReader):
(nr, ns) = subdiv (nr, ns) = subdiv
else: else:
nr = ns = DEFAULT_SUBDIV nr = ns = DEFAULT_SUBDIV
lau = pi / nr # Unit angle of latitude (rings) for the given tesselation lau = pi / nr # Unit angle of latitude (rings) for the given tesselation
lou = 2 * pi / ns # Unit angle of longitude (segments) lou = 2 * pi / ns # Unit angle of longitude (segments)
self.reserveFaceAndVertexCount(ns*(nr*2 - 2), 2 + (nr - 1)*ns) self.reserveFaceAndVertexCount(ns*(nr*2 - 2), 2 + (nr - 1)*ns)
# +y and -y poles # +y and -y poles
self.addVertex(0, r, 0) self.addVertex(0, r, 0)
self.addVertex(0, -r, 0) self.addVertex(0, -r, 0)
# The non-polar vertices go from x=0, negative z plane counterclockwise - # The non-polar vertices go from x=0, negative z plane counterclockwise -
# to -x, to +z, to +x, back to -z # to -x, to +z, to +x, back to -z
for ring in range(1, nr): for ring in range(1, nr):
@ -264,12 +265,12 @@ class X3DReader(MeshReader):
self.addVertex(-r*sin(lou * seg) * sin(lau * ring), self.addVertex(-r*sin(lou * seg) * sin(lau * ring),
r*cos(lau * ring), r*cos(lau * ring),
-r*cos(lou * seg) * sin(lau * ring)) -r*cos(lou * seg) * sin(lau * ring))
vb = 2 + (nr - 2) * ns # First vertex index for the bottom cap vb = 2 + (nr - 2) * ns # First vertex index for the bottom cap
# Faces go in order: top cap, sides, bottom cap. # Faces go in order: top cap, sides, bottom cap.
# Sides go by ring then by segment. # Sides go by ring then by segment.
# Caps # Caps
# Top cap face vertices go in order: down right up # Top cap face vertices go in order: down right up
# (starting from +y pole) # (starting from +y pole)
@ -277,7 +278,7 @@ class X3DReader(MeshReader):
for seg in range(ns): for seg in range(ns):
self.addTri(0, seg + 2, (seg + 1) % ns + 2) self.addTri(0, seg + 2, (seg + 1) % ns + 2)
self.addTri(1, vb + (seg + 1) % ns, vb + seg) self.addTri(1, vb + (seg + 1) % ns, vb + seg)
# Sides # Sides
# Side face vertices go in order: down right upleft, downright up left # Side face vertices go in order: down right upleft, downright up left
for ring in range(nr - 2): for ring in range(nr - 2):
@ -288,24 +289,24 @@ class X3DReader(MeshReader):
for seg in range(ns): for seg in range(ns):
nseg = (seg + 1) % ns nseg = (seg + 1) % ns
self.addQuad(tvb + seg, bvb + seg, bvb + nseg, tvb + nseg) self.addQuad(tvb + seg, bvb + seg, bvb + nseg, tvb + nseg)
def processGeometryCone(self, node): def processGeometryCone(self, node):
r = readFloat(node, "bottomRadius", 1) r = readFloat(node, "bottomRadius", 1)
height = readFloat(node, "height", 2) height = readFloat(node, "height", 2)
bottom = readBoolean(node, "bottom", True) bottom = readBoolean(node, "bottom", True)
side = readBoolean(node, "side", True) side = readBoolean(node, "side", True)
n = readInt(node, "subdivision", DEFAULT_SUBDIV) n = readInt(node, "subdivision", DEFAULT_SUBDIV)
d = height / 2 d = height / 2
angle = 2 * pi / n angle = 2 * pi / n
self.reserveFaceAndVertexCount((n if side else 0) + (n-2 if bottom else 0), n+1) self.reserveFaceAndVertexCount((n if side else 0) + (n-2 if bottom else 0), n+1)
# Vertex 0 is the apex, vertices 1..n are the bottom # Vertex 0 is the apex, vertices 1..n are the bottom
self.addVertex(0, d, 0) self.addVertex(0, d, 0)
for i in range(n): for i in range(n):
self.addVertex(-r * sin(angle * i), -d, -r * cos(angle * i)) self.addVertex(-r * sin(angle * i), -d, -r * cos(angle * i))
# Side face vertices go: up down right # Side face vertices go: up down right
if side: if side:
for i in range(n): for i in range(n):
@ -313,7 +314,7 @@ class X3DReader(MeshReader):
if bottom: if bottom:
for i in range(2, n): for i in range(2, n):
self.addTri(1, i, i+1) self.addTri(1, i, i+1)
def processGeometryCylinder(self, node): def processGeometryCylinder(self, node):
r = readFloat(node, "radius", 1) r = readFloat(node, "radius", 1)
height = readFloat(node, "height", 2) height = readFloat(node, "height", 2)
@ -321,13 +322,13 @@ class X3DReader(MeshReader):
side = readBoolean(node, "side", True) side = readBoolean(node, "side", True)
top = readBoolean(node, "top", True) top = readBoolean(node, "top", True)
n = readInt(node, "subdivision", DEFAULT_SUBDIV) n = readInt(node, "subdivision", DEFAULT_SUBDIV)
nn = n * 2 nn = n * 2
angle = 2 * pi / n angle = 2 * pi / n
hh = height/2 hh = height/2
self.reserveFaceAndVertexCount((nn if side else 0) + (n - 2 if top else 0) + (n - 2 if bottom else 0), nn) self.reserveFaceAndVertexCount((nn if side else 0) + (n - 2 if top else 0) + (n - 2 if bottom else 0), nn)
# The seam is at x=0, z=-r, vertices go ccw - # The seam is at x=0, z=-r, vertices go ccw -
# to pos x, to neg z, to neg x, back to neg z # to pos x, to neg z, to neg x, back to neg z
for i in range(n): for i in range(n):
@ -335,18 +336,18 @@ class X3DReader(MeshReader):
rc = -r * cos(angle * i) rc = -r * cos(angle * i)
self.addVertex(rs, hh, rc) self.addVertex(rs, hh, rc)
self.addVertex(rs, -hh, rc) self.addVertex(rs, -hh, rc)
if side: if side:
for i in range(n): for i in range(n):
ni = (i + 1) % n ni = (i + 1) % n
self.addQuad(ni * 2 + 1, ni * 2, i * 2, i * 2 + 1) self.addQuad(ni * 2 + 1, ni * 2, i * 2, i * 2 + 1)
for i in range(2, nn-3, 2): for i in range(2, nn-3, 2):
if top: if top:
self.addTri(0, i, i+2) self.addTri(0, i, i+2)
if bottom: if bottom:
self.addTri(1, i+1, i+3) self.addTri(1, i+1, i+3)
# Semi-primitives # Semi-primitives
def processGeometryElevationGrid(self, node): def processGeometryElevationGrid(self, node):
@ -356,21 +357,21 @@ class X3DReader(MeshReader):
nz = readInt(node, "zDimension", 0) nz = readInt(node, "zDimension", 0)
height = readFloatArray(node, "height", False) height = readFloatArray(node, "height", False)
ccw = readBoolean(node, "ccw", True) ccw = readBoolean(node, "ccw", True)
if nx <= 0 or nz <= 0 or len(height) < nx*nz: if nx <= 0 or nz <= 0 or len(height) < nx*nz:
return # That's weird, the wording of the standard suggests grids with zero quads are somehow valid return # That's weird, the wording of the standard suggests grids with zero quads are somehow valid
self.reserveFaceAndVertexCount(2*(nx-1)*(nz-1), nx*nz) self.reserveFaceAndVertexCount(2*(nx-1)*(nz-1), nx*nz)
for z in range(nz): for z in range(nz):
for x in range(nx): for x in range(nx):
self.addVertex(x * dx, height[z*nx + x], z * dz) self.addVertex(x * dx, height[z*nx + x], z * dz)
for z in range(1, nz): for z in range(1, nz):
for x in range(1, nx): for x in range(1, nx):
self.addTriFlip((z - 1)*nx + x - 1, z*nx + x, (z - 1)*nx + x, ccw) self.addTriFlip((z - 1)*nx + x - 1, z*nx + x, (z - 1)*nx + x, ccw)
self.addTriFlip((z - 1)*nx + x - 1, z*nx + x - 1, z*nx + x, ccw) self.addTriFlip((z - 1)*nx + x - 1, z*nx + x - 1, z*nx + x, ccw)
def processGeometryExtrusion(self, node): def processGeometryExtrusion(self, node):
ccw = readBoolean(node, "ccw", True) ccw = readBoolean(node, "ccw", True)
begin_cap = readBoolean(node, "beginCap", True) begin_cap = readBoolean(node, "beginCap", True)
@ -384,23 +385,23 @@ class X3DReader(MeshReader):
# This converts X3D's axis/angle rotation to a 3x3 numpy matrix # This converts X3D's axis/angle rotation to a 3x3 numpy matrix
def toRotationMatrix(rot): def toRotationMatrix(rot):
(x, y, z) = rot[:3] (x, y, z) = rot[:3]
a = rot[3] a = rot[3]
s = sin(a) s = sin(a)
c = cos(a) c = cos(a)
t = 1-c t = 1-c
return numpy.array(( return numpy.array((
(x * x * t + c, x * y * t - z*s, x * z * t + y * s), (x * x * t + c, x * y * t - z*s, x * z * t + y * s),
(x * y * t + z*s, y * y * t + c, y * z * t - x * s), (x * y * t + z*s, y * y * t + c, y * z * t - x * s),
(x * z * t - y * s, y * z * t + x * s, z * z * t + c))) (x * z * t - y * s, y * z * t + x * s, z * z * t + c)))
orient = [toRotationMatrix(orient[i:i+4]) if orient[i+3] != 0 else None for i in range(0, len(orient), 4)] orient = [toRotationMatrix(orient[i:i+4]) if orient[i+3] != 0 else None for i in range(0, len(orient), 4)]
scale = readFloatArray(node, "scale", None) scale = readFloatArray(node, "scale", None)
if scale: if scale:
scale = [numpy.array(((scale[i], 0, 0), (0, 1, 0), (0, 0, scale[i+1]))) scale = [numpy.array(((scale[i], 0, 0), (0, 1, 0), (0, 0, scale[i+1])))
if scale[i] != 1 or scale[i+1] != 1 else None for i in range(0, len(scale), 2)] if scale[i] != 1 or scale[i+1] != 1 else None for i in range(0, len(scale), 2)]
# Special treatment for the closed spine and cross section. # Special treatment for the closed spine and cross section.
# Let's save some memory by not creating identical but distinct vertices; # Let's save some memory by not creating identical but distinct vertices;
# later we'll introduce conditional logic to link the last vertex with # later we'll introduce conditional logic to link the last vertex with
@ -413,14 +414,14 @@ class X3DReader(MeshReader):
ncf = nc if crossClosed else nc - 1 ncf = nc if crossClosed else nc - 1
# Face count along the cross; for closed cross, it's the same as the # Face count along the cross; for closed cross, it's the same as the
# respective vertex count # respective vertex count
spine_closed = spine[0] == spine[-1] spine_closed = spine[0] == spine[-1]
if spine_closed: if spine_closed:
spine = spine[:-1] spine = spine[:-1]
ns = len(spine) ns = len(spine)
spine = [Vector(*s) for s in spine] spine = [Vector(*s) for s in spine]
nsf = ns if spine_closed else ns - 1 nsf = ns if spine_closed else ns - 1
# This will be used for fallback, where the current spine point joins # This will be used for fallback, where the current spine point joins
# two collinear spine segments. No need to recheck the case of the # two collinear spine segments. No need to recheck the case of the
# closed spine/last-to-first point juncture; if there's an angle there, # closed spine/last-to-first point juncture; if there's an angle there,
@ -442,7 +443,7 @@ class X3DReader(MeshReader):
if v.cross(orig_y).length() > EPSILON: if v.cross(orig_y).length() > EPSILON:
# Spine at angle with global y - rotate the z accordingly # Spine at angle with global y - rotate the z accordingly
a = v.cross(orig_y) # Axis of rotation to get to the Z a = v.cross(orig_y) # Axis of rotation to get to the Z
(x, y, z) = a.normalized().getData() (x, y, z) = a.normalized().getData()
s = a.length()/v.length() s = a.length()/v.length()
c = sqrt(1-s*s) c = sqrt(1-s*s)
t = 1-c t = 1-c
@ -452,7 +453,7 @@ class X3DReader(MeshReader):
(x * z * t + y * s, y * z * t - x * s, z * z * t + c))) (x * z * t + y * s, y * z * t - x * s, z * z * t + c)))
orig_z = Vector(*m.dot(orig_z.getData())) orig_z = Vector(*m.dot(orig_z.getData()))
return orig_z return orig_z
self.reserveFaceAndVertexCount(2*nsf*ncf + (nc - 2 if begin_cap else 0) + (nc - 2 if end_cap else 0), ns*nc) self.reserveFaceAndVertexCount(2*nsf*ncf + (nc - 2 if begin_cap else 0) + (nc - 2 if end_cap else 0), ns*nc)
z = None z = None
@ -482,151 +483,151 @@ class X3DReader(MeshReader):
y = spt - sprev y = spt - sprev
# If there's more than one point in the spine, z is already set. # If there's more than one point in the spine, z is already set.
# One point in the spline is an error anyway. # One point in the spline is an error anyway.
z = z.normalized() z = z.normalized()
y = y.normalized() y = y.normalized()
x = y.cross(z) # Already normalized x = y.cross(z) # Already normalized
m = numpy.array(((x.x, y.x, z.x), (x.y, y.y, z.y), (x.z, y.z, z.z))) m = numpy.array(((x.x, y.x, z.x), (x.y, y.y, z.y), (x.z, y.z, z.z)))
# Columns are the unit vectors for the xz plane for the cross-section # Columns are the unit vectors for the xz plane for the cross-section
if orient: if orient:
mrot = orient[i] if len(orient) > 1 else orient[0] mrot = orient[i] if len(orient) > 1 else orient[0]
if not mrot is None: if not mrot is None:
m = m.dot(mrot) # Tested against X3DOM, the result matches, still not sure :( m = m.dot(mrot) # Tested against X3DOM, the result matches, still not sure :(
if scale: if scale:
mscale = scale[i] if len(scale) > 1 else scale[0] mscale = scale[i] if len(scale) > 1 else scale[0]
if not mscale is None: if not mscale is None:
m = m.dot(mscale) m = m.dot(mscale)
# First the cross-section 2-vector is scaled, # First the cross-section 2-vector is scaled,
# then rotated (which may make it a 3-vector), # then rotated (which may make it a 3-vector),
# then applied to the xz plane unit vectors # then applied to the xz plane unit vectors
sptv3 = numpy.array(spt.getData()[:3]) sptv3 = numpy.array(spt.getData()[:3])
for cpt in cross: for cpt in cross:
v = sptv3 + m.dot(cpt) v = sptv3 + m.dot(cpt)
self.addVertex(*v) self.addVertex(*v)
if begin_cap: if begin_cap:
self.addFace([x for x in range(nc - 1, -1, -1)], ccw) self.addFace([x for x in range(nc - 1, -1, -1)], ccw)
# Order of edges in the face: forward along cross, forward along spine, # Order of edges in the face: forward along cross, forward along spine,
# backward along cross, backward along spine, flipped if now ccw. # backward along cross, backward along spine, flipped if now ccw.
# This order is assumed later in the texture coordinate assignment; # This order is assumed later in the texture coordinate assignment;
# please don't change without syncing. # please don't change without syncing.
for s in range(ns - 1): for s in range(ns - 1):
for c in range(ncf): for c in range(ncf):
self.addQuadFlip(s * nc + c, s * nc + (c + 1) % nc, self.addQuadFlip(s * nc + c, s * nc + (c + 1) % nc,
(s + 1) * nc + (c + 1) % nc, (s + 1) * nc + c, ccw) (s + 1) * nc + (c + 1) % nc, (s + 1) * nc + c, ccw)
if spine_closed: if spine_closed:
# The faces between the last and the first spine points # The faces between the last and the first spine points
b = (ns - 1) * nc b = (ns - 1) * nc
for c in range(ncf): for c in range(ncf):
self.addQuadFlip(b + c, b + (c + 1) % nc, self.addQuadFlip(b + c, b + (c + 1) % nc,
(c + 1) % nc, c, ccw) (c + 1) % nc, c, ccw)
if end_cap: if end_cap:
self.addFace([(ns - 1) * nc + x for x in range(0, nc)], ccw) self.addFace([(ns - 1) * nc + x for x in range(0, nc)], ccw)
# Triangle meshes # Triangle meshes
# Helper for numerous nodes with a Coordinate subnode holding vertices # Helper for numerous nodes with a Coordinate subnode holding vertices
# That all triangle meshes and IndexedFaceSet # That all triangle meshes and IndexedFaceSet
# num_faces can be a function, in case the face count is a function of vertex count # num_faces can be a function, in case the face count is a function of vertex count
def startCoordMesh(self, node, num_faces): def startCoordMesh(self, node, num_faces):
ccw = readBoolean(node, "ccw", True) ccw = readBoolean(node, "ccw", True)
self.readVertices(node) # This will allocate and fill the vertex array self.readVertices(node) # This will allocate and fill the vertex array
if hasattr(num_faces, "__call__"): if hasattr(num_faces, "__call__"):
num_faces = num_faces(self.getVertexCount()) num_faces = num_faces(self.getVertexCount())
self.reserveFaceCount(num_faces) self.reserveFaceCount(num_faces)
return ccw return ccw
def processGeometryIndexedTriangleSet(self, node): def processGeometryIndexedTriangleSet(self, node):
index = readIntArray(node, "index", []) index = readIntArray(node, "index", [])
num_faces = len(index) // 3 num_faces = len(index) // 3
ccw = int(self.startCoordMesh(node, num_faces)) ccw = int(self.startCoordMesh(node, num_faces))
for i in range(0, num_faces*3, 3): for i in range(0, num_faces*3, 3):
self.addTri(index[i + 1 - ccw], index[i + ccw], index[i+2]) self.addTri(index[i + 1 - ccw], index[i + ccw], index[i+2])
def processGeometryIndexedTriangleStripSet(self, node): def processGeometryIndexedTriangleStripSet(self, node):
strips = readIndex(node, "index") strips = readIndex(node, "index")
ccw = int(self.startCoordMesh(node, sum([len(strip) - 2 for strip in strips]))) ccw = int(self.startCoordMesh(node, sum([len(strip) - 2 for strip in strips])))
for strip in strips: for strip in strips:
sccw = ccw # Running CCW value, reset for each strip sccw = ccw # Running CCW value, reset for each strip
for i in range(len(strip) - 2): for i in range(len(strip) - 2):
self.addTri(strip[i + 1 - sccw], strip[i + sccw], strip[i+2]) self.addTri(strip[i + 1 - sccw], strip[i + sccw], strip[i+2])
sccw = 1 - sccw sccw = 1 - sccw
def processGeometryIndexedTriangleFanSet(self, node): def processGeometryIndexedTriangleFanSet(self, node):
fans = readIndex(node, "index") fans = readIndex(node, "index")
ccw = int(self.startCoordMesh(node, sum([len(fan) - 2 for fan in fans]))) ccw = int(self.startCoordMesh(node, sum([len(fan) - 2 for fan in fans])))
for fan in fans: for fan in fans:
for i in range(1, len(fan) - 1): for i in range(1, len(fan) - 1):
self.addTri(fan[0], fan[i + 1 - ccw], fan[i + ccw]) self.addTri(fan[0], fan[i + 1 - ccw], fan[i + ccw])
def processGeometryTriangleSet(self, node): def processGeometryTriangleSet(self, node):
ccw = int(self.startCoordMesh(node, lambda num_vert: num_vert // 3)) ccw = int(self.startCoordMesh(node, lambda num_vert: num_vert // 3))
for i in range(0, self.getVertexCount(), 3): for i in range(0, self.getVertexCount(), 3):
self.addTri(i + 1 - ccw, i + ccw, i+2) self.addTri(i + 1 - ccw, i + ccw, i+2)
def processGeometryTriangleStripSet(self, node): def processGeometryTriangleStripSet(self, node):
strips = readIntArray(node, "stripCount", []) strips = readIntArray(node, "stripCount", [])
ccw = int(self.startCoordMesh(node, sum([n-2 for n in strips]))) ccw = int(self.startCoordMesh(node, sum([n-2 for n in strips])))
vb = 0 vb = 0
for n in strips: for n in strips:
sccw = ccw sccw = ccw
for i in range(n-2): for i in range(n-2):
self.addTri(vb + i + 1 - sccw, vb + i + sccw, vb + i + 2) self.addTri(vb + i + 1 - sccw, vb + i + sccw, vb + i + 2)
sccw = 1 - sccw sccw = 1 - sccw
vb += n vb += n
def processGeometryTriangleFanSet(self, node): def processGeometryTriangleFanSet(self, node):
fans = readIntArray(node, "fanCount", []) fans = readIntArray(node, "fanCount", [])
ccw = int(self.startCoordMesh(node, sum([n-2 for n in fans]))) ccw = int(self.startCoordMesh(node, sum([n-2 for n in fans])))
vb = 0 vb = 0
for n in fans: for n in fans:
for i in range(1, n-1): for i in range(1, n-1):
self.addTri(vb, vb + i + 1 - ccw, vb + i + ccw) self.addTri(vb, vb + i + 1 - ccw, vb + i + ccw)
vb += n vb += n
# Quad geometries from the CAD module, might be relevant for printing # Quad geometries from the CAD module, might be relevant for printing
def processGeometryQuadSet(self, node): def processGeometryQuadSet(self, node):
ccw = self.startCoordMesh(node, lambda num_vert: 2*(num_vert // 4)) ccw = self.startCoordMesh(node, lambda num_vert: 2*(num_vert // 4))
for i in range(0, self.getVertexCount(), 4): for i in range(0, self.getVertexCount(), 4):
self.addQuadFlip(i, i+1, i+2, i+3, ccw) self.addQuadFlip(i, i+1, i+2, i+3, ccw)
def processGeometryIndexedQuadSet(self, node): def processGeometryIndexedQuadSet(self, node):
index = readIntArray(node, "index", []) index = readIntArray(node, "index", [])
num_quads = len(index) // 4 num_quads = len(index) // 4
ccw = self.startCoordMesh(node, num_quads*2) ccw = self.startCoordMesh(node, num_quads*2)
for i in range(0, num_quads*4, 4): for i in range(0, num_quads*4, 4):
self.addQuadFlip(index[i], index[i+1], index[i+2], index[i+3], ccw) self.addQuadFlip(index[i], index[i+1], index[i+2], index[i+3], ccw)
# 2D polygon geometries # 2D polygon geometries
# Won't work for now, since Cura expects every mesh to have a nontrivial convex hull # Won't work for now, since Cura expects every mesh to have a nontrivial convex hull
# The only way around that is merging meshes. # The only way around that is merging meshes.
def processGeometryDisk2D(self, node): def processGeometryDisk2D(self, node):
innerRadius = readFloat(node, "innerRadius", 0) innerRadius = readFloat(node, "innerRadius", 0)
outerRadius = readFloat(node, "outerRadius", 1) outerRadius = readFloat(node, "outerRadius", 1)
n = readInt(node, "subdivision", DEFAULT_SUBDIV) n = readInt(node, "subdivision", DEFAULT_SUBDIV)
angle = 2 * pi / n angle = 2 * pi / n
self.reserveFaceAndVertexCount(n*4 if innerRadius else n-2, n*2 if innerRadius else n) self.reserveFaceAndVertexCount(n*4 if innerRadius else n-2, n*2 if innerRadius else n)
for i in range(n): for i in range(n):
s = sin(angle * i) s = sin(angle * i)
c = cos(angle * i) c = cos(angle * i)
@ -635,11 +636,11 @@ class X3DReader(MeshReader):
self.addVertex(innerRadius*c, innerRadius*s, 0) self.addVertex(innerRadius*c, innerRadius*s, 0)
ni = (i+1) % n ni = (i+1) % n
self.addQuad(2*i, 2*ni, 2*ni+1, 2*i+1) self.addQuad(2*i, 2*ni, 2*ni+1, 2*i+1)
if not innerRadius: if not innerRadius:
for i in range(2, n): for i in range(2, n):
self.addTri(0, i-1, i) self.addTri(0, i-1, i)
def processGeometryRectangle2D(self, node): def processGeometryRectangle2D(self, node):
(x, y) = readFloatArray(node, "size", (2, 2)) (x, y) = readFloatArray(node, "size", (2, 2))
self.reserveFaceAndVertexCount(2, 4) self.reserveFaceAndVertexCount(2, 4)
@ -648,7 +649,7 @@ class X3DReader(MeshReader):
self.addVertex(x/2, y/2, 0) self.addVertex(x/2, y/2, 0)
self.addVertex(-x/2, y/2, 0) self.addVertex(-x/2, y/2, 0)
self.addQuad(0, 1, 2, 3) self.addQuad(0, 1, 2, 3)
def processGeometryTriangleSet2D(self, node): def processGeometryTriangleSet2D(self, node):
verts = readFloatArray(node, "vertices", ()) verts = readFloatArray(node, "vertices", ())
num_faces = len(verts) // 6; num_faces = len(verts) // 6;
@ -656,25 +657,25 @@ class X3DReader(MeshReader):
self.reserveFaceAndVertexCount(num_faces, num_faces * 3) self.reserveFaceAndVertexCount(num_faces, num_faces * 3)
for vert in verts: for vert in verts:
self.addVertex(*vert) self.addVertex(*vert)
# The front face is on the +Z side, so CCW is a variable # The front face is on the +Z side, so CCW is a variable
for i in range(0, num_faces*3, 3): for i in range(0, num_faces*3, 3):
a = Vector(*verts[i+2]) - Vector(*verts[i]) a = Vector(*verts[i+2]) - Vector(*verts[i])
b = Vector(*verts[i+1]) - Vector(*verts[i]) b = Vector(*verts[i+1]) - Vector(*verts[i])
self.addTriFlip(i, i+1, i+2, a.x*b.y > a.y*b.x) self.addTriFlip(i, i+1, i+2, a.x*b.y > a.y*b.x)
# General purpose polygon mesh # General purpose polygon mesh
def processGeometryIndexedFaceSet(self, node): def processGeometryIndexedFaceSet(self, node):
faces = readIndex(node, "coordIndex") faces = readIndex(node, "coordIndex")
ccw = self.startCoordMesh(node, sum([len(face) - 2 for face in faces])) ccw = self.startCoordMesh(node, sum([len(face) - 2 for face in faces]))
for face in faces: for face in faces:
if len(face) == 3: if len(face) == 3:
self.addTriFlip(face[0], face[1], face[2], ccw) self.addTriFlip(face[0], face[1], face[2], ccw)
elif len(face) > 3: elif len(face) > 3:
self.addFace(face, ccw) self.addFace(face, ccw)
geometry_importers = { geometry_importers = {
"IndexedFaceSet": processGeometryIndexedFaceSet, "IndexedFaceSet": processGeometryIndexedFaceSet,
"IndexedTriangleSet": processGeometryIndexedTriangleSet, "IndexedTriangleSet": processGeometryIndexedTriangleSet,
@ -695,7 +696,7 @@ class X3DReader(MeshReader):
"Cylinder": processGeometryCylinder, "Cylinder": processGeometryCylinder,
"Cone": processGeometryCone "Cone": processGeometryCone
} }
# Parses the Coordinate.@point field, fills the verts array. # Parses the Coordinate.@point field, fills the verts array.
def readVertices(self, node): def readVertices(self, node):
for c in node: for c in node:
@ -704,9 +705,9 @@ class X3DReader(MeshReader):
if not c is None: if not c is None:
pt = c.attrib.get("point") pt = c.attrib.get("point")
if pt: if pt:
# allow the list of float values in 'point' attribute to # allow the list of float values in 'point' attribute to
# be separated by commas or whitespace as per spec of # be separated by commas or whitespace as per spec of
# XML encoding of X3D # XML encoding of X3D
# Ref ISO/IEC 19776-1:2015 : Section 5.1.2 # Ref ISO/IEC 19776-1:2015 : Section 5.1.2
co = [float(x) for vec in pt.split(',') for x in vec.split()] co = [float(x) for vec in pt.split(',') for x in vec.split()]
num_verts = len(co) // 3 num_verts = len(co) // 3
@ -715,57 +716,57 @@ class X3DReader(MeshReader):
# Group by three # Group by three
for i in range(num_verts): for i in range(num_verts):
self.verts[:3,i] = co[3*i:3*i+3] self.verts[:3,i] = co[3*i:3*i+3]
# Mesh builder helpers # Mesh builder helpers
def reserveFaceAndVertexCount(self, num_faces, num_verts): def reserveFaceAndVertexCount(self, num_faces, num_verts):
# Unlike the Cura MeshBuilder, we use 4-vectors stored as columns for easier transform # Unlike the Cura MeshBuilder, we use 4-vectors stored as columns for easier transform
self.verts = numpy.zeros((4, num_verts), dtype=numpy.float32) self.verts = numpy.zeros((4, num_verts), dtype=numpy.float32)
self.verts[3,:] = numpy.ones((num_verts), dtype=numpy.float32) self.verts[3,:] = numpy.ones((num_verts), dtype=numpy.float32)
self.num_verts = 0 self.num_verts = 0
self.reserveFaceCount(num_faces) self.reserveFaceCount(num_faces)
def reserveFaceCount(self, num_faces): def reserveFaceCount(self, num_faces):
self.faces = numpy.zeros((num_faces, 3), dtype=numpy.int32) self.faces = numpy.zeros((num_faces, 3), dtype=numpy.int32)
self.num_faces = 0 self.num_faces = 0
def getVertexCount(self): def getVertexCount(self):
return self.verts.shape[1] return self.verts.shape[1]
def addVertex(self, x, y, z): def addVertex(self, x, y, z):
self.verts[0, self.num_verts] = x self.verts[0, self.num_verts] = x
self.verts[1, self.num_verts] = y self.verts[1, self.num_verts] = y
self.verts[2, self.num_verts] = z self.verts[2, self.num_verts] = z
self.num_verts += 1 self.num_verts += 1
# Indices are 0-based for this shape, but they won't be zero-based in the merged mesh # Indices are 0-based for this shape, but they won't be zero-based in the merged mesh
def addTri(self, a, b, c): def addTri(self, a, b, c):
self.faces[self.num_faces, 0] = self.index_base + a self.faces[self.num_faces, 0] = self.index_base + a
self.faces[self.num_faces, 1] = self.index_base + b self.faces[self.num_faces, 1] = self.index_base + b
self.faces[self.num_faces, 2] = self.index_base + c self.faces[self.num_faces, 2] = self.index_base + c
self.num_faces += 1 self.num_faces += 1
def addTriFlip(self, a, b, c, ccw): def addTriFlip(self, a, b, c, ccw):
if ccw: if ccw:
self.addTri(a, b, c) self.addTri(a, b, c)
else: else:
self.addTri(b, a, c) self.addTri(b, a, c)
# Needs to be convex, but not necessaily planar # Needs to be convex, but not necessaily planar
# Assumed ccw, cut along the ac diagonal # Assumed ccw, cut along the ac diagonal
def addQuad(self, a, b, c, d): def addQuad(self, a, b, c, d):
self.addTri(a, b, c) self.addTri(a, b, c)
self.addTri(c, d, a) self.addTri(c, d, a)
def addQuadFlip(self, a, b, c, d, ccw): def addQuadFlip(self, a, b, c, d, ccw):
if ccw: if ccw:
self.addTri(a, b, c) self.addTri(a, b, c)
self.addTri(c, d, a) self.addTri(c, d, a)
else: else:
self.addTri(a, c, b) self.addTri(a, c, b)
self.addTri(c, a, d) self.addTri(c, a, d)
# Arbitrary polygon triangulation. # Arbitrary polygon triangulation.
# Doesn't assume convexity and doesn't check the "convex" flag in the file. # Doesn't assume convexity and doesn't check the "convex" flag in the file.
# Works by the "cutting of ears" algorithm: # Works by the "cutting of ears" algorithm:
@ -776,13 +777,13 @@ class X3DReader(MeshReader):
def addFace(self, indices, ccw): def addFace(self, indices, ccw):
# Resolve indices to coordinates for faster math # Resolve indices to coordinates for faster math
face = [Vector(data=self.verts[0:3, i]) for i in indices] face = [Vector(data=self.verts[0:3, i]) for i in indices]
# Need a normal to the plane so that we can know which vertices form inner angles # Need a normal to the plane so that we can know which vertices form inner angles
normal = findOuterNormal(face) normal = findOuterNormal(face)
if not normal: # Couldn't find an outer edge, non-planar polygon maybe? if not normal: # Couldn't find an outer edge, non-planar polygon maybe?
return return
# Find the vertex with the smallest inner angle and no points inside, cut off. Repeat until done # Find the vertex with the smallest inner angle and no points inside, cut off. Repeat until done
n = len(face) n = len(face)
vi = [i for i in range(n)] # We'll be using this to kick vertices from the face vi = [i for i in range(n)] # We'll be using this to kick vertices from the face
@ -807,17 +808,17 @@ class X3DReader(MeshReader):
if pointInsideTriangle(vx, next, prev, nextXprev): if pointInsideTriangle(vx, next, prev, nextXprev):
no_points_inside = False no_points_inside = False
break break
if no_points_inside: if no_points_inside:
max_cos = cos max_cos = cos
i_min = i i_min = i
self.addTriFlip(indices[vi[(i_min + n - 1) % n]], indices[vi[i_min]], indices[vi[(i_min + 1) % n]], ccw) self.addTriFlip(indices[vi[(i_min + n - 1) % n]], indices[vi[i_min]], indices[vi[(i_min + 1) % n]], ccw)
vi.pop(i_min) vi.pop(i_min)
n -= 1 n -= 1
self.addTriFlip(indices[vi[0]], indices[vi[1]], indices[vi[2]], ccw) self.addTriFlip(indices[vi[0]], indices[vi[1]], indices[vi[2]], ccw)
# ------------------------------------------------------------ # ------------------------------------------------------------
# X3D field parsers # X3D field parsers
# ------------------------------------------------------------ # ------------------------------------------------------------
@ -844,7 +845,7 @@ def readInt(node, attr, default):
if not s: if not s:
return default return default
return int(s, 0) return int(s, 0)
def readBoolean(node, attr, default): def readBoolean(node, attr, default):
s = node.attrib.get(attr) s = node.attrib.get(attr)
if not s: if not s:
@ -873,8 +874,8 @@ def readIndex(node, attr):
chunk.append(v[i]) chunk.append(v[i])
if chunk: if chunk:
chunks.append(chunk) chunks.append(chunk)
return chunks return chunks
# Given a face as a sequence of vectors, returns a normal to the polygon place that forms a right triple # Given a face as a sequence of vectors, returns a normal to the polygon place that forms a right triple
# with a vector along the polygon sequence and a vector backwards # with a vector along the polygon sequence and a vector backwards
def findOuterNormal(face): def findOuterNormal(face):
@ -894,25 +895,25 @@ def findOuterNormal(face):
if rejection.dot(prev_rejection) < -EPSILON: # points on both sides of the edge - not an outer one if rejection.dot(prev_rejection) < -EPSILON: # points on both sides of the edge - not an outer one
is_outer = False is_outer = False
break break
elif rejection.length() > prev_rejection.length(): # Pick a greater rejection for numeric stability elif rejection.length() > prev_rejection.length(): # Pick a greater rejection for numeric stability
prev_rejection = rejection prev_rejection = rejection
if is_outer: # Found an outer edge, prev_rejection is the rejection inside the face. Generate a normal. if is_outer: # Found an outer edge, prev_rejection is the rejection inside the face. Generate a normal.
return edge.cross(prev_rejection) return edge.cross(prev_rejection)
return False return False
# Given two *collinear* vectors a and b, returns the coefficient that takes b to a. # Given two *collinear* vectors a and b, returns the coefficient that takes b to a.
# No error handling. # No error handling.
# For stability, taking the ration between the biggest coordinates would be better... # For stability, taking the ration between the biggest coordinates would be better...
def ratio(a, b): def ratio(a, b):
if b.x > EPSILON or b.x < -EPSILON: if b.x > EPSILON or b.x < -EPSILON:
return a.x / b.x return a.x / b.x
elif b.y > EPSILON or b.y < -EPSILON: elif b.y > EPSILON or b.y < -EPSILON:
return a.y / b.y return a.y / b.y
else: else:
return a.z / b.z return a.z / b.z
def pointInsideTriangle(vx, next, prev, nextXprev): def pointInsideTriangle(vx, next, prev, nextXprev):
vxXprev = vx.cross(prev) vxXprev = vx.cross(prev)
r = ratio(vxXprev, nextXprev) r = ratio(vxXprev, nextXprev)
@ -921,4 +922,4 @@ def pointInsideTriangle(vx, next, prev, nextXprev):
vxXnext = vx.cross(next); vxXnext = vx.cross(next);
s = -ratio(vxXnext, nextXprev) s = -ratio(vxXnext, nextXprev)
return s > 0 and (s + r) < 1 return s > 0 and (s + r) < 1

View file

@ -409,6 +409,7 @@ UM.MainWindow
{ {
id: objectsList; id: objectsList;
visible: false; visible: false;
//z: -10;
anchors anchors
{ {
top: objectsButton.top; top: objectsButton.top;

View file

@ -55,7 +55,7 @@ Rectangle
//anchors.right: parent.right //anchors.right: parent.right
width: parent.width - 2 * UM.Theme.getSize("default_margin").width - 30 width: parent.width - 2 * UM.Theme.getSize("default_margin").width - 30
text: Cura.ObjectManager.getItem(index).name; text: Cura.ObjectManager.getItem(index).name;
color: Cura.ObjectManager.getItem(index).isSelected ? palette.highlightedText : palette.text color: Cura.ObjectManager.getItem(index).isSelected ? palette.highlightedText : (Cura.ObjectManager.getItem(index).isOutsideBuildArea ? palette.mid : palette.text)
elide: Text.ElideRight elide: Text.ElideRight
} }
@ -95,7 +95,7 @@ Rectangle
topMargin: UM.Theme.getSize("default_margin").height; topMargin: UM.Theme.getSize("default_margin").height;
left: parent.left; left: parent.left;
leftMargin: UM.Theme.getSize("default_margin").height; leftMargin: UM.Theme.getSize("default_margin").height;
bottom: buildPlateSelection.top; bottom: filterBuildPlateCheckbox.top;
bottomMargin: UM.Theme.getSize("default_margin").height; bottomMargin: UM.Theme.getSize("default_margin").height;
} }
@ -115,6 +115,25 @@ Rectangle
} }
} }
CheckBox
{
id: filterBuildPlateCheckbox
checked: boolCheck(UM.Preferences.getValue("view/filter_current_build_plate"))
onClicked: UM.Preferences.setValue("view/filter_current_build_plate", checked)
text: catalog.i18nc("@option:check","Filter active build plate");
anchors
{
left: parent.left;
topMargin: UM.Theme.getSize("default_margin").height;
bottomMargin: UM.Theme.getSize("default_margin").height;
leftMargin: UM.Theme.getSize("default_margin").height;
bottom: buildPlateSelection.top;
}
}
ListModel ListModel
{ {
id: buildPlatesModel id: buildPlatesModel

View file

@ -304,7 +304,7 @@ UM.PreferencesPage
text: catalog.i18nc("@option:check","Slice automatically"); text: catalog.i18nc("@option:check","Slice automatically");
} }
} }
Item Item
{ {
//: Spacer //: Spacer
@ -451,6 +451,20 @@ UM.PreferencesPage
text: catalog.i18nc("@label","Opening and saving files") text: catalog.i18nc("@label","Opening and saving files")
} }
UM.TooltipArea {
width: childrenRect.width
height: childrenRect.height
text: catalog.i18nc("@info:tooltip","Should newly loaded models be arranged on the build palte?")
CheckBox
{
id: arrangeOnLoadCheckbox
text: catalog.i18nc("@option:check","Arrange objects on load")
checked: boolCheck(UM.Preferences.getValue("cura/arrange_objects_on_load"))
onCheckedChanged: UM.Preferences.setValue("cura/arrange_objects_on_load", checked)
}
}
UM.TooltipArea { UM.TooltipArea {
width: childrenRect.width width: childrenRect.width
height: childrenRect.height height: childrenRect.height