mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-06 22:47:29 -06:00
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:
parent
41d5ec86a3
commit
e21acd1a07
18 changed files with 468 additions and 260 deletions
|
@ -40,7 +40,7 @@ class Arrange:
|
|||
# \param fixed_nodes Scene nodes to be placed
|
||||
@classmethod
|
||||
def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 220, y = 220):
|
||||
arranger = Arrange(x, y, x / 2, y / 2, scale = scale)
|
||||
arranger = Arrange(x, y, x // 2, y // 2, scale = scale)
|
||||
arranger.centerFirst()
|
||||
|
||||
if fixed_nodes is None:
|
||||
|
|
|
@ -112,7 +112,6 @@ class ArrangeObjectsAllBuildPlatesJob(Job):
|
|||
# start_priority = 0
|
||||
|
||||
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
|
||||
while current_build_plate_number >= arrange_array.count():
|
||||
arrange_array.add()
|
||||
|
|
|
@ -6,7 +6,6 @@ from UM.Scene.SceneNode import SceneNode
|
|||
from UM.Resources import Resources
|
||||
from UM.Math.Color import Color
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder # To create a mesh to display the convex hull with.
|
||||
|
||||
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_opacity", 0.6)
|
||||
|
||||
if self.getParent():
|
||||
if self.getParent() and self.getParent().callDecoration("getBuildPlateNumber") == Application.getInstance().activeBuildPlate:
|
||||
if self.getMeshData():
|
||||
renderer.queueNode(self, transparent = True, shader = ConvexHullNode.shader, backface_cull = True, sort = -8)
|
||||
if self._convex_hull_head_mesh:
|
||||
|
|
|
@ -33,6 +33,7 @@ from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
|||
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
||||
from UM.Operations.GroupedOperation import GroupedOperation
|
||||
from UM.Operations.SetTransformOperation import SetTransformOperation
|
||||
|
||||
from cura.Arrange import Arrange
|
||||
from cura.ShapeArray import ShapeArray
|
||||
from cura.ConvexHullDecorator import ConvexHullDecorator
|
||||
|
@ -41,6 +42,7 @@ from cura.SliceableObjectDecorator import SliceableObjectDecorator
|
|||
from cura.BlockSlicingDecorator import BlockSlicingDecorator
|
||||
# research
|
||||
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
|
||||
from cura.ArrangeObjectsJob import ArrangeObjectsJob
|
||||
from cura.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
|
||||
|
@ -307,11 +309,13 @@ class CuraApplication(QtApplication):
|
|||
preferences.addPreference("cura/asked_dialog_on_project_save", False)
|
||||
preferences.addPreference("cura/choice_on_profile_override", "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/material_settings", "{}")
|
||||
|
||||
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")
|
||||
|
||||
|
@ -896,7 +900,7 @@ class CuraApplication(QtApplication):
|
|||
scene_bounding_box = None
|
||||
is_block_slicing_node = False
|
||||
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
|
||||
if node.callDecoration("isBlockSlicing"):
|
||||
is_block_slicing_node = True
|
||||
|
@ -1013,7 +1017,7 @@ class CuraApplication(QtApplication):
|
|||
|
||||
Selection.clear()
|
||||
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.
|
||||
|
@ -1021,6 +1025,9 @@ class CuraApplication(QtApplication):
|
|||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||
if not node.isSelectable():
|
||||
continue # i.e. node with layer data
|
||||
if not node.callDecoration("isSliceable"):
|
||||
continue # i.e. node with layer data
|
||||
|
||||
Selection.add(node)
|
||||
|
||||
## Delete all nodes containing mesh data in the scene.
|
||||
|
@ -1032,7 +1039,7 @@ class CuraApplication(QtApplication):
|
|||
|
||||
nodes = []
|
||||
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("getLayerData")) and not node.callDecoration("isGroup"):
|
||||
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")
|
||||
nodes = []
|
||||
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.
|
||||
|
@ -1082,13 +1089,13 @@ class CuraApplication(QtApplication):
|
|||
Logger.log("i", "Resetting all scene transformations")
|
||||
nodes = []
|
||||
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.isSelectable():
|
||||
if not node.callDecoration("isSliceable"):
|
||||
continue # i.e. node with layer data
|
||||
nodes.append(node)
|
||||
|
||||
|
@ -1109,7 +1116,27 @@ class CuraApplication(QtApplication):
|
|||
def arrangeObjectsToAllBuildPlates(self):
|
||||
nodes = []
|
||||
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
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
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)
|
||||
if not node.isSelectable():
|
||||
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()
|
||||
|
||||
# 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():
|
||||
if not node.callDecoration("isSliceable"):
|
||||
continue # i.e. node with layer data
|
||||
if node.callDecoration("getBuildPlateNumber") == self._active_build_plate:
|
||||
# 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
|
||||
fixed_nodes = []
|
||||
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.
|
||||
|
@ -1158,6 +1168,8 @@ class CuraApplication(QtApplication):
|
|||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||
if not node.isSelectable():
|
||||
continue # i.e. node with layer data
|
||||
if not node.callDecoration("isSliceable"):
|
||||
continue # i.e. node with layer data
|
||||
if node in nodes: # exclude selected node from fixed_nodes
|
||||
continue
|
||||
fixed_nodes.append(node)
|
||||
|
@ -1176,7 +1188,7 @@ class CuraApplication(QtApplication):
|
|||
Logger.log("i", "Reloading all loaded mesh data.")
|
||||
nodes = []
|
||||
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
|
||||
|
||||
nodes.append(node)
|
||||
|
@ -1267,7 +1279,7 @@ class CuraApplication(QtApplication):
|
|||
@pyqtSlot()
|
||||
def groupSelected(self):
|
||||
# Create a group-node
|
||||
group_node = SceneNode()
|
||||
group_node = CuraSceneNode()
|
||||
group_decorator = GroupDecorator()
|
||||
group_node.addDecorator(group_decorator)
|
||||
group_node.addDecorator(ConvexHullDecorator())
|
||||
|
@ -1413,11 +1425,15 @@ class CuraApplication(QtApplication):
|
|||
min_offset = 8
|
||||
|
||||
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.setName(os.path.basename(filename))
|
||||
node.addDecorator(BuildPlateDecorator())
|
||||
|
||||
extension = os.path.splitext(filename)[1]
|
||||
if extension.lower() in self._non_sliceable_extensions:
|
||||
|
@ -1442,6 +1458,7 @@ class CuraApplication(QtApplication):
|
|||
if not child.getDecorator(ConvexHullDecorator):
|
||||
child.addDecorator(ConvexHullDecorator())
|
||||
|
||||
if arrange_objects_on_load:
|
||||
if node.callDecoration("isSliceable"):
|
||||
# Only check position if it's not already blatantly obvious that it won't fit.
|
||||
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
|
||||
|
@ -1457,6 +1474,8 @@ class CuraApplication(QtApplication):
|
|||
# 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.addDecorator(BuildPlateDecorator(target_build_plate))
|
||||
|
||||
op = AddSceneNodeOperation(node, scene.getRoot())
|
||||
op.push()
|
||||
scene.sceneChanged.emit(node)
|
||||
|
@ -1494,6 +1513,8 @@ class CuraApplication(QtApplication):
|
|||
#### research - hacky place for these kind of thing
|
||||
@pyqtSlot(int)
|
||||
def setActiveBuildPlate(self, nr):
|
||||
if nr == self._active_build_plate:
|
||||
return
|
||||
Logger.log("d", "Select build plate: %s" % nr)
|
||||
self._active_build_plate = nr
|
||||
|
||||
|
|
|
@ -7,23 +7,35 @@ from UM.Scene.SceneNode import SceneNode
|
|||
from UM.Scene.Selection import Selection
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
#from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
from UM.Preferences import Preferences
|
||||
|
||||
|
||||
class ObjectManager(ListModel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
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):
|
||||
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()):
|
||||
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
|
||||
nodes.append({
|
||||
"name": node.getName(),
|
||||
"isSelected": Selection.isSelected(node),
|
||||
"buildPlateNumber": node.callDecoration("getBuildPlateNumber"),
|
||||
"isOutsideBuildArea": node.isOutsideBuildArea(),
|
||||
"buildPlateNumber": node_build_plate_number,
|
||||
"node": node
|
||||
})
|
||||
nodes = sorted(nodes, key=lambda n: n["name"])
|
||||
|
@ -31,6 +43,12 @@ class ObjectManager(ListModel):
|
|||
|
||||
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
|
||||
@pyqtSlot(int)
|
||||
def changeSelection(self, index):
|
||||
|
@ -63,6 +81,11 @@ class ObjectManager(ListModel):
|
|||
|
||||
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
|
||||
def createObjectManager():
|
||||
return ObjectManager()
|
||||
|
|
|
@ -6,11 +6,14 @@ from UM.Logger import Logger
|
|||
class BuildPlateDecorator(SceneNodeDecorator):
|
||||
def __init__(self, build_plate_number = -1):
|
||||
super().__init__()
|
||||
self._build_plate_number = None
|
||||
self._previous_build_plate_number = None
|
||||
self.setBuildPlateNumber(build_plate_number)
|
||||
|
||||
def setBuildPlateNumber(self, nr):
|
||||
# Make sure that groups are set correctly
|
||||
# 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
|
||||
if self._node and self._node.callDecoration("isGroup"):
|
||||
for child in self._node.getChildren():
|
||||
|
@ -19,5 +22,9 @@ class BuildPlateDecorator(SceneNodeDecorator):
|
|||
def getBuildPlateNumber(self):
|
||||
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):
|
||||
return BuildPlateDecorator()
|
||||
|
|
40
cura/Scene/CuraSceneNode.py
Normal file
40
cura/Scene/CuraSceneNode.py
Normal 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
|
|
@ -15,7 +15,8 @@ from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
|||
from UM.Application import Application
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
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.ZOffsetDecorator import ZOffsetDecorator
|
||||
|
||||
|
|
|
@ -69,9 +69,10 @@ class CuraEngineBackend(QObject, Backend):
|
|||
# Workaround to disable layer view processing if layer view is not active.
|
||||
self._layer_view_active = False
|
||||
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
|
||||
Application.getInstance().activeBuildPlateChanged.connect(self._onActiveViewChanged)
|
||||
self._onActiveViewChanged()
|
||||
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.sceneChanged.connect(self._onSceneChanged)
|
||||
|
@ -104,12 +105,14 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage
|
||||
|
||||
self._start_slice_job = None
|
||||
self._start_slice_job_build_plate = None
|
||||
self._slicing = False # Are we currently slicing?
|
||||
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._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._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._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.
|
||||
def slice(self):
|
||||
Logger.log("d", "starting to slice again!")
|
||||
self._slice_start_time = time()
|
||||
if not self._need_slicing:
|
||||
if not self._build_plates_to_be_sliced:
|
||||
self.processingProgress.emit(1.0)
|
||||
self.backendStateChange.emit(BackendState.Done)
|
||||
Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.")
|
||||
|
@ -199,7 +203,6 @@ class CuraEngineBackend(QObject, Backend):
|
|||
Application.getInstance().getPrintInformation().setToZeroPrintInformation()
|
||||
|
||||
self._stored_layer_data = []
|
||||
self._stored_optimized_layer_data = []
|
||||
|
||||
if self._process is None:
|
||||
self._createSocket()
|
||||
|
@ -215,6 +218,9 @@ class CuraEngineBackend(QObject, Backend):
|
|||
|
||||
slice_message = self._socket.createMessage("cura.proto.Slice")
|
||||
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.finished.connect(self._onStartSliceCompleted)
|
||||
|
||||
|
@ -223,7 +229,8 @@ class CuraEngineBackend(QObject, Backend):
|
|||
def _terminate(self):
|
||||
self._slicing = False
|
||||
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:
|
||||
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."),
|
||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||
self._error_message.show()
|
||||
self.backendStateChange.emit(BackendState.Error)
|
||||
#self.backendStateChange.emit(BackendState.Error)
|
||||
else:
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
#self.backendStateChange.emit(BackendState.NotStarted)
|
||||
pass
|
||||
self._invokeSlice()
|
||||
return
|
||||
|
||||
# Preparation completed, send it to the backend.
|
||||
self._socket.sendMessage(job.getSliceMessage())
|
||||
|
||||
|
@ -360,27 +370,34 @@ class CuraEngineBackend(QObject, Backend):
|
|||
#
|
||||
# \param source The scene node that was changed.
|
||||
def _onSceneChanged(self, source):
|
||||
if type(source) is not SceneNode:
|
||||
Logger.log("d", " ##### scene changed: %s", source)
|
||||
if not issubclass(type(source), SceneNode):
|
||||
return
|
||||
|
||||
root_scene_nodes_changed = False
|
||||
build_plates_changed = set()
|
||||
if source == self._scene.getRoot():
|
||||
num_objects = 0
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
# Only count sliceable objects
|
||||
if node.callDecoration("isSliceable"):
|
||||
num_objects += 1
|
||||
build_plates_changed.add(node.callDecoration("getBuildPlateNumber"))
|
||||
build_plates_changed.add(node.callDecoration("getPreviousBuildPlateNumber"))
|
||||
if num_objects != self._last_num_objects:
|
||||
self._last_num_objects = num_objects
|
||||
root_scene_nodes_changed = True
|
||||
else:
|
||||
return
|
||||
# else:
|
||||
# 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 source.getMeshData() is None:
|
||||
return
|
||||
if source.getMeshData().getVertices() is None:
|
||||
return
|
||||
# if not source.callDecoration("isGroup") and not root_scene_nodes_changed:
|
||||
# if source.getMeshData() is None:
|
||||
# return
|
||||
# if source.getMeshData().getVertices() is None:
|
||||
# return
|
||||
|
||||
if self._tool_active:
|
||||
# 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)
|
||||
return
|
||||
|
||||
self.needsSlicing()
|
||||
if build_plates_changed:
|
||||
Logger.log("d", " going to reslice")
|
||||
self.stopSlicing()
|
||||
self._onChanged()
|
||||
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.
|
||||
#
|
||||
|
@ -410,16 +442,24 @@ class CuraEngineBackend(QObject, Backend):
|
|||
Logger.log("w", "A socket error caused the connection to be reset")
|
||||
|
||||
## Remove old layer data (if any)
|
||||
def _clearLayerData(self):
|
||||
def _clearLayerData(self, build_plate_numbers = set()):
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if node.callDecoration("getLayerData"):
|
||||
if node.callDecoration("getBuildPlateNumber") in build_plate_numbers or not build_plate_numbers:
|
||||
node.getParent().removeChild(node)
|
||||
break
|
||||
|
||||
## 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):
|
||||
self.stopSlicing()
|
||||
self._need_slicing = True
|
||||
self.markSliceAll()
|
||||
self.processingProgress.emit(0.0)
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
if not self._use_timer:
|
||||
|
@ -441,7 +481,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
|
||||
def _onStackErrorCheckFinished(self):
|
||||
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._onChanged()
|
||||
|
||||
|
@ -455,7 +495,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
#
|
||||
# \param message The protobuf message containing sliced layer data.
|
||||
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.
|
||||
#
|
||||
|
@ -464,6 +504,16 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self.processingProgress.emit(message.amount)
|
||||
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.
|
||||
#
|
||||
# \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._slicing = False
|
||||
self._need_slicing = False
|
||||
#self._need_slicing = False
|
||||
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)
|
||||
self._process_layers_job.finished.connect(self._onProcessLayersFinished)
|
||||
self._process_layers_job.start()
|
||||
self._stored_optimized_layer_data = []
|
||||
|
||||
# See if we need to process the sliced layers job.
|
||||
active_build_plate = Application.getInstance().activeBuildPlate
|
||||
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._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.
|
||||
#
|
||||
|
@ -584,19 +641,26 @@ class CuraEngineBackend(QObject, Backend):
|
|||
source = self._postponed_scene_change_sources.pop(0)
|
||||
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.
|
||||
def _onActiveViewChanged(self):
|
||||
if Application.getInstance().getController().getActiveView():
|
||||
view = Application.getInstance().getController().getActiveView()
|
||||
application = Application.getInstance()
|
||||
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.
|
||||
self._layer_view_active = True
|
||||
# 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 self._stored_optimized_layer_data and not self._slicing:
|
||||
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data)
|
||||
self._process_layers_job.finished.connect(self._onProcessLayersFinished)
|
||||
self._process_layers_job.start()
|
||||
self._stored_optimized_layer_data = []
|
||||
# TODO: what build plate I am slicing
|
||||
if active_build_plate in self._stored_optimized_layer_data and not self._slicing:
|
||||
self._startProcessSlicedLayersJob(active_build_plate)
|
||||
else:
|
||||
self._layer_view_active = False
|
||||
|
||||
|
|
|
@ -17,10 +17,12 @@ from UM.Logger import Logger
|
|||
|
||||
from UM.Math.Vector import Vector
|
||||
|
||||
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from cura import LayerDataBuilder
|
||||
from cura import LayerDataDecorator
|
||||
from cura import LayerPolygon
|
||||
# from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
|
||||
import numpy
|
||||
from time import time
|
||||
|
@ -49,6 +51,7 @@ class ProcessSlicedLayersJob(Job):
|
|||
self._scene = Application.getInstance().getController().getScene()
|
||||
self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1)
|
||||
self._abort_requested = False
|
||||
self._build_plate_number = None
|
||||
|
||||
## Aborts the processing of layers.
|
||||
#
|
||||
|
@ -59,7 +62,11 @@ class ProcessSlicedLayersJob(Job):
|
|||
def abort(self):
|
||||
self._abort_requested = True
|
||||
|
||||
def setBuildPlate(self, new_value):
|
||||
self._build_plate_number = new_value
|
||||
|
||||
def run(self):
|
||||
Logger.log("d", "########## Processing new layer for [%s]..." % self._build_plate_number)
|
||||
start_time = time()
|
||||
if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView":
|
||||
self._progress_message.show()
|
||||
|
@ -72,16 +79,18 @@ class ProcessSlicedLayersJob(Job):
|
|||
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
|
||||
|
||||
new_node = SceneNode()
|
||||
new_node.addDecorator(BuildPlateDecorator(self._build_plate_number))
|
||||
|
||||
## Remove old layer data (if any)
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if node.callDecoration("getLayerData"):
|
||||
node.getParent().removeChild(node)
|
||||
break
|
||||
if self._abort_requested:
|
||||
if self._progress_message:
|
||||
self._progress_message.hide()
|
||||
return
|
||||
# ## Remove old layer data (if any)
|
||||
# for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
# if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number:
|
||||
# Logger.log("d", " # Removing: %s", node)
|
||||
# node.getParent().removeChild(node)
|
||||
# #break
|
||||
# if self._abort_requested:
|
||||
# if self._progress_message:
|
||||
# self._progress_message.hide()
|
||||
# return
|
||||
|
||||
# Force garbage collection.
|
||||
# For some reason, Python has a tendency to keep the layer data
|
||||
|
|
|
@ -10,12 +10,13 @@ from UM.Job import Job
|
|||
from UM.Application import Application
|
||||
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.Settings.Validator import ValidatorState
|
||||
from UM.Settings.SettingRelation import RelationType
|
||||
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode
|
||||
from cura.OneAtATimeIterator import OneAtATimeIterator
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
|
@ -58,10 +59,14 @@ class StartSliceJob(Job):
|
|||
self._scene = Application.getInstance().getController().getScene()
|
||||
self._slice_message = slice_message
|
||||
self._is_cancelled = False
|
||||
self._build_plate_number = None
|
||||
|
||||
def getSliceMessage(self):
|
||||
return self._slice_message
|
||||
|
||||
def setBuildPlate(self, build_plate_number):
|
||||
self._build_plate_number = build_plate_number
|
||||
|
||||
## Check if a stack has any errors.
|
||||
## returns true if it has errors, false otherwise.
|
||||
def _checkStackForErrors(self, stack):
|
||||
|
@ -78,6 +83,10 @@ class StartSliceJob(Job):
|
|||
|
||||
## Runs the job that initiates the slicing.
|
||||
def run(self):
|
||||
if self._build_plate_number is None:
|
||||
self.setResult(StartJobResult.Error)
|
||||
return
|
||||
|
||||
stack = Application.getInstance().getGlobalContainerStack()
|
||||
if not stack:
|
||||
self.setResult(StartJobResult.Error)
|
||||
|
@ -141,14 +150,12 @@ class StartSliceJob(Job):
|
|||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
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") == 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 (node.callDecoration("getBuildPlateNumber") == self._build_plate_number):
|
||||
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)
|
||||
Job.yieldThread()
|
||||
|
||||
Logger.log("d", " objects to be sliced: %s", temp_list)
|
||||
if temp_list:
|
||||
object_groups.append(temp_list)
|
||||
|
||||
|
|
|
@ -8,12 +8,14 @@ from PyQt5.QtCore import Qt
|
|||
|
||||
from UM.Mesh.MeshReader import MeshReader
|
||||
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.Job import Job
|
||||
from UM.Logger import Logger
|
||||
from .ImageReaderUI import ImageReaderUI
|
||||
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode
|
||||
|
||||
|
||||
class ImageReader(MeshReader):
|
||||
def __init__(self):
|
||||
|
|
|
@ -66,13 +66,14 @@ class LayerPass(RenderPass):
|
|||
self.bind()
|
||||
|
||||
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()):
|
||||
|
||||
if isinstance(node, ToolHandle):
|
||||
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")
|
||||
if not layer_data:
|
||||
continue
|
||||
|
|
|
@ -75,7 +75,7 @@ class SolidView(View):
|
|||
|
||||
for node in DepthFirstIterator(scene.getRoot()):
|
||||
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 = {}
|
||||
shade_factor = 1.0
|
||||
|
||||
|
|
|
@ -11,7 +11,8 @@ from UM.Math.Matrix import Matrix
|
|||
from UM.Math.Vector import Vector
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
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
|
||||
try:
|
||||
|
|
|
@ -409,6 +409,7 @@ UM.MainWindow
|
|||
{
|
||||
id: objectsList;
|
||||
visible: false;
|
||||
//z: -10;
|
||||
anchors
|
||||
{
|
||||
top: objectsButton.top;
|
||||
|
|
|
@ -55,7 +55,7 @@ Rectangle
|
|||
//anchors.right: parent.right
|
||||
width: parent.width - 2 * UM.Theme.getSize("default_margin").width - 30
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -95,7 +95,7 @@ Rectangle
|
|||
topMargin: UM.Theme.getSize("default_margin").height;
|
||||
left: parent.left;
|
||||
leftMargin: UM.Theme.getSize("default_margin").height;
|
||||
bottom: buildPlateSelection.top;
|
||||
bottom: filterBuildPlateCheckbox.top;
|
||||
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
|
||||
{
|
||||
id: buildPlatesModel
|
||||
|
|
|
@ -451,6 +451,20 @@ UM.PreferencesPage
|
|||
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 {
|
||||
width: childrenRect.width
|
||||
height: childrenRect.height
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue