From 7aefb671728f07616730bba71fd11e9e4577123b Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Tue, 10 May 2016 13:19:53 +0200 Subject: [PATCH 01/18] Make Vector an immutable class. Contributes to CURA-1504 --- cura/PlatformPhysics.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index 78f2b4938b..e2bca741a3 100644 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -67,9 +67,9 @@ class PlatformPhysics: if not (node.getParent() and node.getParent().callDecoration("isGroup")): #If an object is grouped, don't move it down z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0 if bbox.bottom > 0: - move_vector.setY(-bbox.bottom + z_offset) + move_vector = move_vector.set(y=-bbox.bottom + z_offset) elif bbox.bottom < z_offset: - move_vector.setY((-bbox.bottom) - z_offset) + move_vector = move_vector.set(y=(-bbox.bottom) - z_offset) #if not Float.fuzzyCompare(bbox.bottom, 0.0): # pass#move_vector.setY(-bbox.bottom) @@ -125,8 +125,7 @@ class PlatformPhysics: if overlap is None: continue - move_vector.setX(overlap[0] * 1.1) - move_vector.setZ(overlap[1] * 1.1) + move_vector = move_vector.set(x=overlap[0] * 1.1, z=overlap[1] * 1.1) convex_hull = node.callDecoration("getConvexHull") if convex_hull: if not convex_hull.isValid(): From 43747f3f57aa4e0cc874cd372a7b72ed20794b31 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Thu, 12 May 2016 13:12:44 +0200 Subject: [PATCH 02/18] Fix up BuildVolume's override of the AABB. Contributes to CURA-1504 --- cura/BuildVolume.py | 6 +++++- cura/PlatformPhysics.py | 4 ---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index e700b8d7be..7072533ab9 100644 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -36,6 +36,7 @@ class BuildVolume(SceneNode): self._disallowed_area_mesh = None self.setCalculateBoundingBox(False) + self._volume_aabb = None self._active_profile = None self._active_instance = None @@ -144,7 +145,7 @@ class BuildVolume(SceneNode): else: self._disallowed_area_mesh = None - self._aabb = AxisAlignedBox(minimum = Vector(min_w, min_h - 1.0, min_d), maximum = Vector(max_w, max_h, max_d)) + self._volume_aabb = AxisAlignedBox(minimum = Vector(min_w, min_h - 1.0, min_d), maximum = Vector(max_w, max_h, max_d)) skirt_size = 0.0 @@ -162,6 +163,9 @@ class BuildVolume(SceneNode): Application.getInstance().getController().getScene()._maximum_bounds = scale_to_max_bounds + def getBoundingBox(self): + return self._volume_aabb + def _onActiveInstanceChanged(self): self._active_instance = Application.getInstance().getMachineManager().getActiveMachineInstance() diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index e2bca741a3..7b2a47eef5 100644 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -27,7 +27,6 @@ class PlatformPhysics: self._controller.toolOperationStarted.connect(self._onToolOperationStarted) self._controller.toolOperationStopped.connect(self._onToolOperationStopped) self._build_volume = volume - self._enabled = True self._change_timer = QTimer() @@ -50,9 +49,6 @@ class PlatformPhysics: continue bbox = node.getBoundingBox() - if not bbox or not bbox.isValid(): - self._change_timer.start() - continue build_volume_bounding_box = copy.deepcopy(self._build_volume.getBoundingBox()) build_volume_bounding_box.setBottom(-9001) # Ignore intersections with the bottom From bac58ecc82968df88b43650f7c86f2d61098e953 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Thu, 12 May 2016 13:16:14 +0200 Subject: [PATCH 03/18] Prevent models from vibrating on the z axis due to fp errors, change the tolerance of the comparison. Contributes to CURA-1504 --- cura/PlatformPhysics.py | 2 +- cura/PlatformPhysicsOperation.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index 7b2a47eef5..55cc3652e1 100644 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -134,7 +134,7 @@ class PlatformPhysics: node._outside_buildarea = True - if move_vector != Vector(): + if not Vector.Null.equals(move_vector, epsilon=1e-5): op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector) op.push() diff --git a/cura/PlatformPhysicsOperation.py b/cura/PlatformPhysicsOperation.py index 5d2089e8af..fa58e45d9c 100644 --- a/cura/PlatformPhysicsOperation.py +++ b/cura/PlatformPhysicsOperation.py @@ -28,4 +28,4 @@ class PlatformPhysicsOperation(Operation): return group def __repr__(self): - return "PlatformPhysicsOperation(t = {0})".format(self._position) + return "PlatformPhysicsOperation(new_position = {0})".format(self._new_position) From dfccbf636a1a27ec4efb4e4b30f476bc05559147 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Thu, 19 May 2016 15:10:31 +0200 Subject: [PATCH 04/18] Fixed up the ImageReader after the breaking changes else where. Contributes to CURA-1504 --- plugins/ImageReader/ImageReader.py | 34 ++++++++++++++---------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/plugins/ImageReader/ImageReader.py b/plugins/ImageReader/ImageReader.py index 0d6c12b13d..9d70dde8e1 100644 --- a/plugins/ImageReader/ImageReader.py +++ b/plugins/ImageReader/ImageReader.py @@ -7,7 +7,7 @@ from PyQt5.QtGui import QImage, qRed, qGreen, qBlue from PyQt5.QtCore import Qt from UM.Mesh.MeshReader import MeshReader -from UM.Mesh.MeshData import MeshData +from UM.Mesh.MeshBuilder import MeshBuilder from UM.Scene.SceneNode import SceneNode from UM.Math.Vector import Vector from UM.Job import Job @@ -48,13 +48,9 @@ class ImageReader(MeshReader): return self._generateSceneNode(file_name, size, self._ui.peak_height, self._ui.base_height, self._ui.smoothing, 512, self._ui.image_color_invert) def _generateSceneNode(self, file_name, xz_size, peak_height, base_height, blur_iterations, max_size, image_color_invert): - mesh = None # TODO: @UnusedVariable - scene_node = None # TODO: @UnusedVariable - scene_node = SceneNode() - mesh = MeshData() - scene_node.setMeshData(mesh) + mesh = MeshBuilder() img = QImage(file_name) @@ -76,9 +72,9 @@ class ImageReader(MeshReader): scale_vector = Vector(xz_size, peak_height, xz_size) if width > height: - scale_vector.setZ(scale_vector.z * aspect) + scale_vector = scale_vector.set(z=scale_vector.z * aspect) elif height > width: - scale_vector.setX(scale_vector.x / aspect) + scale_vector = scale_vector.set(x=scale_vector.x / aspect) if width > max_size or height > max_size: scale_factor = max_size / width @@ -173,8 +169,8 @@ class ImageReader(MeshReader): geo_height = height_minus_one * texel_height # bottom - mesh.addFace(0, 0, 0, 0, 0, geo_height, geo_width, 0, geo_height) - mesh.addFace(geo_width, 0, geo_height, geo_width, 0, 0, 0, 0, 0) + mesh.addFaceByPoints(0, 0, 0, 0, 0, geo_height, geo_width, 0, geo_height) + mesh.addFaceByPoints(geo_width, 0, geo_height, geo_width, 0, 0, 0, 0, 0) # north and south walls for n in range(0, width_minus_one): @@ -187,11 +183,11 @@ class ImageReader(MeshReader): hs0 = height_data[height_minus_one, n] hs1 = height_data[height_minus_one, n + 1] - mesh.addFace(x, 0, 0, nx, 0, 0, nx, hn1, 0) - mesh.addFace(nx, hn1, 0, x, hn0, 0, x, 0, 0) + mesh.addFaceByPoints(x, 0, 0, nx, 0, 0, nx, hn1, 0) + mesh.addFaceByPoints(nx, hn1, 0, x, hn0, 0, x, 0, 0) - mesh.addFace(x, 0, geo_height, nx, 0, geo_height, nx, hs1, geo_height) - mesh.addFace(nx, hs1, geo_height, x, hs0, geo_height, x, 0, geo_height) + mesh.addFaceByPoints(x, 0, geo_height, nx, 0, geo_height, nx, hs1, geo_height) + mesh.addFaceByPoints(nx, hs1, geo_height, x, hs0, geo_height, x, 0, geo_height) # west and east walls for n in range(0, height_minus_one): @@ -204,12 +200,14 @@ class ImageReader(MeshReader): he0 = height_data[n, width_minus_one] he1 = height_data[n + 1, width_minus_one] - mesh.addFace(0, 0, y, 0, 0, ny, 0, hw1, ny) - mesh.addFace(0, hw1, ny, 0, hw0, y, 0, 0, y) + mesh.addFaceByPoints(0, 0, y, 0, 0, ny, 0, hw1, ny) + mesh.addFaceByPoints(0, hw1, ny, 0, hw0, y, 0, 0, y) - mesh.addFace(geo_width, 0, y, geo_width, 0, ny, geo_width, he1, ny) - mesh.addFace(geo_width, he1, ny, geo_width, he0, y, geo_width, 0, y) + mesh.addFaceByPoints(geo_width, 0, y, geo_width, 0, ny, geo_width, he1, ny) + mesh.addFaceByPoints(geo_width, he1, ny, geo_width, he0, y, geo_width, 0, y) mesh.calculateNormals(fast=True) + scene_node.setMeshData(mesh.build()) + return scene_node From a109396c0d3006b3c1b12175b5039df9a4a0ded4 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Thu, 19 May 2016 22:08:10 +0200 Subject: [PATCH 05/18] Update to handle the removal of MeshData.setCenterPosition and MeshData.setVertexUVCoordinates(). Contributes to CURA-1504 --- cura/BuildVolume.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 7072533ab9..dd76095d9a 100644 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -113,10 +113,10 @@ class BuildVolume(SceneNode): Vector(max_w, min_h - 0.2, max_d), Vector(min_w, min_h - 0.2, max_d) ) - self._grid_mesh = mb.getData() for n in range(0, 6): - v = self._grid_mesh.getVertex(n) - self._grid_mesh.setVertexUVCoordinates(n, v[0], v[2]) + v = mb.getVertex(n) + mb.setVertexUVCoordinates(n, v[0], v[2]) + self._grid_mesh = mb.getData() disallowed_area_height = 0.1 disallowed_area_size = 0 From d1f68143a4ca346f6441832127cca26d6c8fbaad Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Sat, 21 May 2016 14:17:58 +0200 Subject: [PATCH 06/18] Adjustments to support immutable AxisAlignedBox. Contributes to CURA-1504 --- cura/CuraApplication.py | 4 ++-- cura/PlatformPhysics.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 9b113b7de8..72ff8aec8c 100644 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -95,7 +95,7 @@ class CuraApplication(QtApplication): self._i18n_catalog = None self._previous_active_tool = None self._platform_activity = False - self._scene_boundingbox = AxisAlignedBox() + self._scene_boundingbox = AxisAlignedBox.Null self._job_name = None self._center_after_select = False self._camera_animation = None @@ -283,7 +283,7 @@ class CuraApplication(QtApplication): scene_boundingbox += node.getBoundingBox() if not scene_boundingbox: - scene_boundingbox = AxisAlignedBox() + scene_boundingbox = AxisAlignedBox.Null if repr(self._scene_boundingbox) != repr(scene_boundingbox): self._scene_boundingbox = scene_boundingbox diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index 55cc3652e1..57d8f4e0ba 100644 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -50,8 +50,8 @@ class PlatformPhysics: bbox = node.getBoundingBox() - build_volume_bounding_box = copy.deepcopy(self._build_volume.getBoundingBox()) - build_volume_bounding_box.setBottom(-9001) # Ignore intersections with the bottom + # Ignore intersections with the bottom + build_volume_bounding_box = self._build_volume.getBoundingBox().set(bottom=-9001) node._outside_buildarea = False # Mark the node as outside the build volume if the bounding box test fails. From 0b858f38781847f79dc85181d27addf3e9365212 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Mon, 23 May 2016 09:49:31 +0200 Subject: [PATCH 07/18] Introduced a LayerDataBuilder. Made LayerData immutable just like its superclass. Fixed the layer view which broke. Contributes to CURA-1504 --- cura/LayerData.py | 61 +++------------- cura/LayerDataBuilder.py | 72 +++++++++++++++++++ .../ProcessSlicedLayersJob.py | 8 +-- plugins/LayerView/LayerView.py | 6 +- 4 files changed, 89 insertions(+), 58 deletions(-) create mode 100644 cura/LayerDataBuilder.py diff --git a/cura/LayerData.py b/cura/LayerData.py index 4aa7d1f4d4..ad5326373e 100644 --- a/cura/LayerData.py +++ b/cura/LayerData.py @@ -1,66 +1,25 @@ # Copyright (c) 2015 Ultimaker B.V. # Cura is released under the terms of the AGPLv3 or higher. -from .Layer import Layer -from .LayerPolygon import LayerPolygon from UM.Mesh.MeshData import MeshData -import numpy - - +## Class to holds the layer mesh and information about the layers. +# Immutable, use LayerDataBuilder to create one of these. class LayerData(MeshData): - def __init__(self): - super().__init__() - self._layers = {} - self._element_counts = {} - - def addLayer(self, layer): - if layer not in self._layers: - self._layers[layer] = Layer(layer) - - def addPolygon(self, layer, polygon_type, data, line_width): - if layer not in self._layers: - self.addLayer(layer) - - p = LayerPolygon(self, polygon_type, data, line_width) - self._layers[layer].polygons.append(p) + def __init__(self, vertices = None, normals = None, indices = None, colors = None, uvs = None, file_name = None, + center_position = None, layers=None, element_counts=None): + super().__init__(vertices=vertices, normals=normals, indices=indices, colors=colors, uvs=uvs, + file_name=file_name, center_position=center_position) + self._layers = layers + self._element_counts = element_counts def getLayer(self, layer): if layer in self._layers: return self._layers[layer] + else: + return None def getLayers(self): return self._layers def getElementCounts(self): return self._element_counts - - def setLayerHeight(self, layer, height): - if layer not in self._layers: - self.addLayer(layer) - - self._layers[layer].setHeight(height) - - def setLayerThickness(self, layer, thickness): - if layer not in self._layers: - self.addLayer(layer) - - self._layers[layer].setThickness(thickness) - - def build(self): - vertex_count = 0 - for layer, data in self._layers.items(): - vertex_count += data.vertexCount() - - vertices = numpy.empty((vertex_count, 3), numpy.float32) - colors = numpy.empty((vertex_count, 4), numpy.float32) - indices = numpy.empty((vertex_count, 2), numpy.int32) - - offset = 0 - for layer, data in self._layers.items(): - offset = data.build(offset, vertices, colors, indices) - self._element_counts[layer] = data.elementCount - - self.clear() - self.addVertices(vertices) - self.addColors(colors) - self.addIndices(indices.flatten()) diff --git a/cura/LayerDataBuilder.py b/cura/LayerDataBuilder.py new file mode 100644 index 0000000000..7e8e0e636b --- /dev/null +++ b/cura/LayerDataBuilder.py @@ -0,0 +1,72 @@ +# Copyright (c) 2015 Ultimaker B.V. +# Cura is released under the terms of the AGPLv3 or higher. + +from .Layer import Layer +from .LayerPolygon import LayerPolygon +from UM.Mesh.MeshBuilder import MeshBuilder +from .LayerData import LayerData + +import numpy + +## Builder class for constructing a LayerData object +class LayerDataBuilder(MeshBuilder): + def __init__(self): + super().__init__() + self._layers = {} + self._element_counts = {} + + def addLayer(self, layer): + if layer not in self._layers: + self._layers[layer] = Layer(layer) + + def addPolygon(self, layer, polygon_type, data, line_width): + if layer not in self._layers: + self.addLayer(layer) + + p = LayerPolygon(self, polygon_type, data, line_width) + self._layers[layer].polygons.append(p) + + def getLayer(self, layer): + if layer in self._layers: + return self._layers[layer] + + def getLayers(self): + return self._layers + + def getElementCounts(self): + return self._element_counts + + def setLayerHeight(self, layer, height): + if layer not in self._layers: + self.addLayer(layer) + + self._layers[layer].setHeight(height) + + def setLayerThickness(self, layer, thickness): + if layer not in self._layers: + self.addLayer(layer) + + self._layers[layer].setThickness(thickness) + + def build(self): + vertex_count = 0 + for layer, data in self._layers.items(): + vertex_count += data.vertexCount() + + vertices = numpy.empty((vertex_count, 3), numpy.float32) + colors = numpy.empty((vertex_count, 4), numpy.float32) + indices = numpy.empty((vertex_count, 2), numpy.int32) + + offset = 0 + for layer, data in self._layers.items(): + offset = data.build(offset, vertices, colors, indices) + self._element_counts[layer] = data.elementCount + + self.addVertices(vertices) + self.addColors(colors) + self.addIndices(indices.flatten()) + + return LayerData(vertices=self.getVertices(), normals=self.getNormals(), indices=self.getIndices(), + colors=self.getColors(), uvs=self.getUVCoordinates(), file_name=self.getFileName(), + center_position=self.getCenterPosition(), layers=self._layers, + element_counts=self._element_counts) diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index 6a947866d3..cea4dedc9f 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -12,7 +12,7 @@ from UM.i18n import i18nCatalog from UM.Math.Vector import Vector -from cura import LayerData +from cura import LayerDataBuilder from cura import LayerDataDecorator import numpy @@ -65,7 +65,7 @@ class ProcessSlicedLayersJob(Job): settings = Application.getInstance().getMachineManager().getWorkingProfile() mesh = MeshData() - layer_data = LayerData.LayerData() + layer_data = LayerDataBuilder.LayerDataBuilder() layer_count = len(self._layers) # Find the minimum layer number @@ -117,7 +117,7 @@ class ProcessSlicedLayersJob(Job): self._progress.setProgress(progress) # We are done processing all the layers we got from the engine, now create a mesh out of the data - layer_data.build() + layer_mesh = layer_data.build() if self._abort_requested: if self._progress: @@ -126,7 +126,7 @@ class ProcessSlicedLayersJob(Job): # Add LayerDataDecorator to scene node to indicate that the node has layer data decorator = LayerDataDecorator.LayerDataDecorator() - decorator.setLayerData(layer_data) + decorator.setLayerData(layer_mesh) new_node.addDecorator(decorator) new_node.setMeshData(mesh) diff --git a/plugins/LayerView/LayerView.py b/plugins/LayerView/LayerView.py index a98fab5c8a..e024512579 100644 --- a/plugins/LayerView/LayerView.py +++ b/plugins/LayerView/LayerView.py @@ -8,7 +8,7 @@ from UM.Event import Event, KeyEvent from UM.Signal import Signal from UM.Scene.Selection import Selection from UM.Math.Color import Color -from UM.Mesh.MeshData import MeshData +from UM.Mesh.MeshBuilder import MeshBuilder from UM.Job import Job from UM.View.RenderBatch import RenderBatch @@ -228,7 +228,7 @@ class _CreateTopLayersJob(Job): if self._cancel or not layer_data: return - layer_mesh = MeshData() + layer_mesh = MeshBuilder() for i in range(self._solid_layers): layer_number = self._layer_number - i if layer_number < 0: @@ -263,7 +263,7 @@ class _CreateTopLayersJob(Job): if not jump_mesh or jump_mesh.getVertices() is None: jump_mesh = None - self.setResult({ "layers": layer_mesh, "jumps": jump_mesh }) + self.setResult({ "layers": layer_mesh.build(), "jumps": jump_mesh }) def cancel(self): self._cancel = True From d7127b800c24f85b7da9f9904d9f4e1eb74c7d97 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Mon, 23 May 2016 13:47:21 +0200 Subject: [PATCH 08/18] Finally, use the new convex hull code to compute the object 'shadow' and exclusion zones. Contributes to CURA-1504 --- cura/ConvexHullDecorator.py | 209 +++++++++++++++++++++++++----------- cura/ConvexHullJob.py | 62 ++++++++++- cura/ConvexHullNode.py | 11 +- cura/CuraApplication.py | 6 +- cura/PlatformPhysics.py | 13 +-- 5 files changed, 222 insertions(+), 79 deletions(-) diff --git a/cura/ConvexHullDecorator.py b/cura/ConvexHullDecorator.py index b53737cc80..1d03c250e5 100644 --- a/cura/ConvexHullDecorator.py +++ b/cura/ConvexHullDecorator.py @@ -1,28 +1,20 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from UM.Application import Application +from UM.Math.Polygon import Polygon +from UM.Logger import Logger +from . import ConvexHullNode + +import numpy ## The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node. # If a scene node has a convex hull decorator, it will have a shadow in which other objects can not be printed. class ConvexHullDecorator(SceneNodeDecorator): - def __init__(self): + def __init__(self,): super().__init__() - self._convex_hull = None - - # In case of printing all at once this is the same as the convex hull. - # For one at the time this is the area without the head. - self._convex_hull_boundary = None - - # In case of printing all at once this is the same as the convex hull. - # For one at the time this is area with intersection of mirrored head - self._convex_hull_head = None - # In case of printing all at once this is the same as the convex hull. - # For one at the time this is area with intersection of full head - self._convex_hull_head_full = None - self._convex_hull_node = None - self._convex_hull_job = None + self._init2DConvexHullCache() self._profile = None Application.getInstance().getMachineManager().activeProfileChanged.connect(self._onActiveProfileChanged) @@ -31,59 +23,56 @@ class ConvexHullDecorator(SceneNodeDecorator): ## Force that a new (empty) object is created upon copy. def __deepcopy__(self, memo): - copy = ConvexHullDecorator() - return copy + return ConvexHullDecorator() - ## Get the unmodified convex hull of the node + ## Get the unmodified 2D projected convex hull of the node def getConvexHull(self): - return self._convex_hull + hull = self._compute2DConvexHull() + profile = Application.getInstance().getMachineManager().getWorkingProfile() + if profile: + if profile.getSettingValue("print_sequence") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"): + hull = hull.getMinkowskiHull(Polygon(numpy.array(profile.getSettingValue("machine_head_polygon"), numpy.float32))) + return hull ## Get the convex hull of the node with the full head size def getConvexHullHeadFull(self): - if not self._convex_hull_head_full: - return self.getConvexHull() - return self._convex_hull_head_full + return self._compute2DConvexHeadFull() ## Get convex hull of the object + head size # In case of printing all at once this is the same as the convex hull. # For one at the time this is area with intersection of mirrored head def getConvexHullHead(self): - if not self._convex_hull_head: - return self.getConvexHull() - return self._convex_hull_head + profile = Application.getInstance().getMachineManager().getWorkingProfile() + if profile: + if profile.getSettingValue("print_sequence") == "one_at_a_time" and not self._node.getParent().callDecoration( + "isGroup"): + return self._compute2DConvexHeadMin() + return None ## Get convex hull of the node # In case of printing all at once this is the same as the convex hull. # For one at the time this is the area without the head. def getConvexHullBoundary(self): - if not self._convex_hull_boundary: - return self.getConvexHull() - return self._convex_hull_boundary - - def setConvexHullBoundary(self, hull): - self._convex_hull_boundary = hull + profile = Application.getInstance().getMachineManager().getWorkingProfile() + if profile: + if profile.getSettingValue("print_sequence") == "one_at_a_time" and not self._node.getParent().callDecoration( + "isGroup"): + # Printing one at a time and it's not an object in a group + return self._compute2DConvexHull() + return None - def setConvexHullHeadFull(self, hull): - self._convex_hull_head_full = hull + def recomputeConvexHull(self): + convex_hull = self.getConvexHull() + if self._convex_hull_node: + if self._convex_hull_node.getHull() == convex_hull: + Logger.log('d', 'ConvexHullDecorator not creating a new ConvexHullNode') + return + self._convex_hull_node.setParent(None) + Logger.log('d', 'ConvexHullDecorator creating ConvexHullNode') + hull_node = ConvexHullNode.ConvexHullNode(self._node, convex_hull, + Application.getInstance().getController().getScene().getRoot()) + self._convex_hull_node = hull_node - def setConvexHullHead(self, hull): - self._convex_hull_head = hull - - def setConvexHull(self, hull): - self._convex_hull = hull - - def getConvexHullJob(self): - return self._convex_hull_job - - def setConvexHullJob(self, job): - self._convex_hull_job = job - - def getConvexHullNode(self): - return self._convex_hull_node - - def setConvexHullNode(self, node): - self._convex_hull_node = node - def _onActiveProfileChanged(self): if self._profile: self._profile.settingValueChanged.disconnect(self._onSettingValueChanged) @@ -94,18 +83,118 @@ class ConvexHullDecorator(SceneNodeDecorator): self._profile.settingValueChanged.connect(self._onSettingValueChanged) def _onActiveMachineInstanceChanged(self): - if self._convex_hull_job: - self._convex_hull_job.cancel() - self.setConvexHull(None) if self._convex_hull_node: self._convex_hull_node.setParent(None) self._convex_hull_node = None def _onSettingValueChanged(self, setting): if setting == "print_sequence": - if self._convex_hull_job: - self._convex_hull_job.cancel() - self.setConvexHull(None) - if self._convex_hull_node: - self._convex_hull_node.setParent(None) - self._convex_hull_node = None + self.recomputeConvexHull() + + def _init2DConvexHullCache(self): + # Cache for the group code path in _compute2DConvexHull() + self._2d_convex_hull_group_child_polygon = None + self._2d_convex_hull_group_result = None + + # Cache for the mesh code path in _compute2DConvexHull() + self._2d_convex_hull_mesh = None + self._2d_convex_hull_mesh_world_transform = None + self._2d_convex_hull_mesh_result = None + + def _compute2DConvexHull(self): + if self._node.callDecoration("isGroup"): + points = numpy.zeros((0, 2), dtype=numpy.int32) + for child in self._node.getChildren(): + child_hull = child.callDecoration("_compute2DConvexHull") + if child_hull: + points = numpy.append(points, child_hull.getPoints(), axis = 0) + + if points.size < 3: + return None + child_polygon = Polygon(points) + + # Check the cache + if child_polygon == self._2d_convex_hull_group_child_polygon: + # Logger.log('d', 'Cache hit in _compute2DConvexHull group path') + return self._2d_convex_hull_group_result + + # First, calculate the normal convex hull around the points + convex_hull = child_polygon.getConvexHull() + + # Then, do a Minkowski hull with a simple 1x1 quad to outset and round the normal convex hull. + # This is done because of rounding errors. + rounded_hull = self._roundHull(convex_hull) + + # Store the result in the cache + self._2d_convex_hull_group_child_polygon = child_polygon + self._2d_convex_hull_group_result = rounded_hull + + return rounded_hull + + else: + if not self._node.getMeshData(): + return None + mesh = self._node.getMeshData() + world_transform = self._node.getWorldTransformation() + + # Check the cache + if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform: + # Logger.log('d', 'Cache hit in _compute2DConvexHull mesh path') + return self._2d_convex_hull_mesh_result + + vertex_data = mesh.getConvexHullTransformedVertices(world_transform) + # Don't use data below 0. + # TODO; We need a better check for this as this gives poor results for meshes with long edges. + vertex_data = vertex_data[vertex_data[:,1] >= 0] + + # Round the vertex data to 1/10th of a mm, then remove all duplicate vertices + # This is done to greatly speed up further convex hull calculations as the convex hull + # becomes much less complex when dealing with highly detailed models. + vertex_data = numpy.round(vertex_data, 1) + + vertex_data = vertex_data[:, [0, 2]] # Drop the Y components to project to 2D. + + # Grab the set of unique points. + # + # This basically finds the unique rows in the array by treating them as opaque groups of bytes + # which are as long as the 2 float64s in each row, and giving this view to numpy.unique() to munch. + # See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array + vertex_byte_view = numpy.ascontiguousarray(vertex_data).view( + numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1]))) + _, idx = numpy.unique(vertex_byte_view, return_index=True) + vertex_data = vertex_data[idx] # Select the unique rows by index. + + hull = Polygon(vertex_data) + + # First, calculate the normal convex hull around the points + convex_hull = hull.getConvexHull() + + # Then, do a Minkowski hull with a simple 1x1 quad to outset and round the normal convex hull. + # This is done because of rounding errors. + rounded_hull = convex_hull.getMinkowskiHull(Polygon(numpy.array([[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]], numpy.float32))) + + # Store the result in the cache + self._2d_convex_hull_mesh = mesh + self._2d_convex_hull_mesh_world_transform = world_transform + self._2d_convex_hull_mesh_result = rounded_hull + + return rounded_hull + + def _getHeadAndFans(self): + profile = Application.getInstance().getMachineManager().getWorkingProfile() + return Polygon(numpy.array(profile.getSettingValue("machine_head_with_fans_polygon"), numpy.float32)) + + def _compute2DConvexHeadFull(self): + return self._compute2DConvexHull().getMinkowskiHull(self._getHeadAndFans()) + + def _compute2DConvexHeadMin(self): + headAndFans = self._getHeadAndFans() + mirrored = headAndFans.mirror([0, 0], [0, 1]).mirror([0, 0], [1, 0]) # Mirror horizontally & vertically. + head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored) + + # Min head hull is used for the push free + min_head_hull = self._compute2DConvexHull().getMinkowskiHull(head_and_fans) + return min_head_hull + + def _roundHull(self, convex_hull): + return convex_hull.getMinkowskiHull(Polygon(numpy.array([[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]], numpy.float32))) diff --git a/cura/ConvexHullJob.py b/cura/ConvexHullJob.py index 9fb18981d3..09d23b7104 100644 --- a/cura/ConvexHullJob.py +++ b/cura/ConvexHullJob.py @@ -4,6 +4,7 @@ from UM.Job import Job from UM.Application import Application from UM.Math.Polygon import Polygon +from UM.Logger import Logger import numpy import copy @@ -19,6 +20,11 @@ class ConvexHullJob(Job): def run(self): if not self._node: return + + ################################################################# + # Node Convex Hull + ################################################################# + ## If the scene node is a group, use the hull of the children to calculate its hull. if self._node.callDecoration("isGroup"): hull = Polygon(numpy.zeros((0, 2), dtype=numpy.int32)) @@ -47,10 +53,20 @@ class ConvexHullJob(Job): # This is done to greatly speed up further convex hull calculations as the convex hull # becomes much less complex when dealing with highly detailed models. vertex_data = numpy.round(vertex_data, 1) - duplicates = (vertex_data[:,0] == vertex_data[:,1]) | (vertex_data[:,1] == vertex_data[:,2]) | (vertex_data[:,0] == vertex_data[:,2]) - vertex_data = numpy.delete(vertex_data, numpy.where(duplicates), axis = 0) - hull = Polygon(vertex_data[:, [0, 2]]) + vertex_data = vertex_data[:, [0, 2]] # Drop the Y components to project to 2D. + + # Grab the set of unique points. + # + # This basically finds the unique rows in the array by treating them as opaque groups of bytes + # which are as long as the 2 float64s in each row, and giving this view to numpy.unique() to munch. + # See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array + vertex_byte_view = numpy.ascontiguousarray(vertex_data).view( + numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1]))) + _, idx = numpy.unique(vertex_byte_view, return_index=True) + vertex_data = vertex_data[idx] # Select the unique rows by index. + + hull = Polygon(vertex_data) # First, calculate the normal convex hull around the points hull = hull.getConvexHull() @@ -59,6 +75,16 @@ class ConvexHullJob(Job): # This is done because of rounding errors. hull = hull.getMinkowskiHull(Polygon(numpy.array([[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]], numpy.float32))) + ################################################################# + # Print Head Exclusion Zone + ################################################################# + + + # + # TODO + # ConvexHullDecorator should use a memoization strategy in its getters. + # Make MeshData immutable + profile = Application.getInstance().getMachineManager().getWorkingProfile() if profile: if profile.getSettingValue("print_sequence") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"): @@ -99,3 +125,33 @@ class ConvexHullJob(Job): hull_node = self._node.getParent().callDecoration("getConvexHullNode") if hull_node: hull_node.setParent(None) + + try: + Logger.log('d', 'ConvexHullJob getConvexHull:' + dumpPoly(self._node.callDecoration("getConvexHull"))) + Logger.log('d', 'ConvexHullJob new getConvexHull:' + dumpPoly(self._node.callDecoration("newGetConvexHull"))) + except Exception: + pass + + try: + Logger.log('d', 'ConvexHullJob getConvexHullHeadFull:' + dumpPoly(self._node.callDecoration("getConvexHullHeadFull"))) + Logger.log('d', 'ConvexHullJob new getConvexHullHeadFull:' + dumpPoly(self._node.callDecoration("newGetConvexHullHeadFull"))) + except Exception: + pass + + try: + Logger.log('d', 'ConvexHullJob getConvexHullHead:' + dumpPoly(self._node.callDecoration("getConvexHullHead"))) + Logger.log('d', 'ConvexHullJob new getConvexHullHead:' + dumpPoly(self._node.callDecoration("newGetConvexHullHead"))) + except Exception: + pass + + try: + Logger.log('d', 'ConvexHullJob getConvexHullBoundary:' + dumpPoly(self._node.callDecoration("getConvexHullBoundary"))) + Logger.log('d', 'ConvexHullJob new getConvexHullBoundary:' + dumpPoly(self._node.callDecoration("newGetConvexHullBoundary"))) + except Exception: + pass + +def dumpPoly(poly): + if poly is None: + return "None" + else: + return repr(poly.getPoints()) diff --git a/cura/ConvexHullNode.py b/cura/ConvexHullNode.py index 905aeb16d4..b5a2df518d 100644 --- a/cura/ConvexHullNode.py +++ b/cura/ConvexHullNode.py @@ -8,7 +8,7 @@ from UM.Math.Vector import Vector 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.Logger import spy class ConvexHullNode(SceneNode): ## Convex hull node is a special type of scene node that is used to display a 2D area, to indicate the @@ -46,6 +46,9 @@ class ConvexHullNode(SceneNode): if convex_hull_head: self._convex_hull_head_mesh = self.createHullMesh(convex_hull_head.getPoints()) + def getHull(self): + return self._hull + ## Actually create the mesh from the hullpoints # /param hull_points list of xy values # /return meshData @@ -62,7 +65,7 @@ class ConvexHullNode(SceneNode): mesh_builder.addFace(point_first, point_previous, point_new, color = self._color) point_previous = point_new # Prepare point_previous for the next triangle. - return mesh_builder.getData() + return mesh_builder.build() def getWatchedNode(self): return self._node @@ -80,9 +83,7 @@ class ConvexHullNode(SceneNode): return True def _onNodePositionChanged(self, node): - if node.callDecoration("getConvexHull"): - node.callDecoration("setConvexHull", None) - node.callDecoration("setConvexHullNode", None) + if node.callDecoration("getConvexHull"): self.setParent(None) # Garbage collection should delete this node after a while. def _onNodeParentChanged(self, node): diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 72ff8aec8c..4977cc799b 100644 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -278,9 +278,11 @@ class CuraApplication(QtApplication): count += 1 if not scene_boundingbox: - scene_boundingbox = copy.deepcopy(node.getBoundingBox()) + scene_boundingbox = node.getBoundingBox() else: - scene_boundingbox += node.getBoundingBox() + other_bb = node.getBoundingBox() + if other_bb is not None: + scene_boundingbox = scene_boundingbox + node.getBoundingBox() if not scene_boundingbox: scene_boundingbox = AxisAlignedBox.Null diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index 57d8f4e0ba..b0863ccfd7 100644 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -45,7 +45,7 @@ class PlatformPhysics: root = self._controller.getScene().getRoot() for node in BreadthFirstIterator(root): - if node is root or type(node) is not SceneNode: + if node is root or type(node) is not SceneNode or node.getBoundingBox() is None: continue bbox = node.getBoundingBox() @@ -73,14 +73,9 @@ class PlatformPhysics: # If there is no convex hull for the node, start calculating it and continue. if not node.getDecorator(ConvexHullDecorator): node.addDecorator(ConvexHullDecorator()) - - if not node.callDecoration("getConvexHull"): - if not node.callDecoration("getConvexHullJob"): - job = ConvexHullJob.ConvexHullJob(node) - job.start() - node.callDecoration("setConvexHullJob", job) - - elif Preferences.getInstance().getValue("physics/automatic_push_free"): + node.callDecoration("recomputeConvexHull") + + if Preferences.getInstance().getValue("physics/automatic_push_free"): # Check for collisions between convex hulls for other_node in BreadthFirstIterator(root): # Ignore root, ourselves and anything that is not a normal SceneNode. From be145d02b3ba43466367f6af0679248b17252d2b Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Tue, 24 May 2016 20:42:21 +0200 Subject: [PATCH 09/18] Updated the cura code for removal of MeshBuilder.getMesh(). Contributes to CURA-1504 --- cura/BuildVolume.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index dd76095d9a..71db735f0a 100644 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -104,7 +104,7 @@ class BuildVolume(SceneNode): mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color = self.VolumeOutlineColor) mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor) - self.setMeshData(mb.getData()) + self.setMeshData(mb.build()) mb = MeshBuilder() mb.addQuad( @@ -116,7 +116,7 @@ class BuildVolume(SceneNode): for n in range(0, 6): v = mb.getVertex(n) mb.setVertexUVCoordinates(n, v[0], v[2]) - self._grid_mesh = mb.getData() + self._grid_mesh = mb.build() disallowed_area_height = 0.1 disallowed_area_size = 0 @@ -141,7 +141,7 @@ class BuildVolume(SceneNode): size = 0 disallowed_area_size = max(size, disallowed_area_size) - self._disallowed_area_mesh = mb.getData() + self._disallowed_area_mesh = mb.build() else: self._disallowed_area_mesh = None From a2b5128c05c9cf48f37a96f53097fc6c8d19cdcf Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Tue, 24 May 2016 20:51:49 +0200 Subject: [PATCH 10/18] Updated the cura code for removal of MeshBuilder.getMesh(). Contributes to CURA-1504 --- cura/Layer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Layer.py b/cura/Layer.py index a95fbf64ef..904e5528a3 100644 --- a/cura/Layer.py +++ b/cura/Layer.py @@ -96,4 +96,4 @@ class Layer: builder.addQuad(point1, point2, point3, point4, color = poly_color) - return builder.getData() \ No newline at end of file + return builder.build() From 3915dec42659d26ed1c6f1aef408ae4fa2564534 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Tue, 24 May 2016 20:56:41 +0200 Subject: [PATCH 11/18] Only remove the old convex hull shadow when a tool is really being used. Contributes to CURA-1504 --- cura/ConvexHullNode.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cura/ConvexHullNode.py b/cura/ConvexHullNode.py index b5a2df518d..3d00edd288 100644 --- a/cura/ConvexHullNode.py +++ b/cura/ConvexHullNode.py @@ -1,6 +1,7 @@ # Copyright (c) 2015 Ultimaker B.V. # Cura is released under the terms of the AGPLv3 or higher. +from UM.Application import Application from UM.Scene.SceneNode import SceneNode from UM.Resources import Resources from UM.Math.Color import Color @@ -8,7 +9,6 @@ from UM.Math.Vector import Vector 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.Logger import spy class ConvexHullNode(SceneNode): ## Convex hull node is a special type of scene node that is used to display a 2D area, to indicate the @@ -83,8 +83,9 @@ class ConvexHullNode(SceneNode): return True def _onNodePositionChanged(self, node): - if node.callDecoration("getConvexHull"): - self.setParent(None) # Garbage collection should delete this node after a while. + if Application.getInstance().getController().isToolOperationActive(): + if node.callDecoration("getConvexHull"): + self.setParent(None) # Garbage collection should delete this node after a while. def _onNodeParentChanged(self, node): if node.getParent(): From 6b71326df9763d228e95175930f944e4e3fb11d9 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Wed, 25 May 2016 15:44:56 +0200 Subject: [PATCH 12/18] Deleted ConvexHullJob. We no longer need it. Contributes to CURA-1504 --- cura/ConvexHullJob.py | 157 ------------------------------------------ 1 file changed, 157 deletions(-) delete mode 100644 cura/ConvexHullJob.py diff --git a/cura/ConvexHullJob.py b/cura/ConvexHullJob.py deleted file mode 100644 index 09d23b7104..0000000000 --- a/cura/ConvexHullJob.py +++ /dev/null @@ -1,157 +0,0 @@ -# Copyright (c) 2015 Ultimaker B.V. -# Cura is released under the terms of the AGPLv3 or higher. - -from UM.Job import Job -from UM.Application import Application -from UM.Math.Polygon import Polygon -from UM.Logger import Logger - -import numpy -import copy -from . import ConvexHullNode - -## Job to async calculate the convex hull of a node. -class ConvexHullJob(Job): - def __init__(self, node): - super().__init__() - - self._node = node - - def run(self): - if not self._node: - return - - ################################################################# - # Node Convex Hull - ################################################################# - - ## If the scene node is a group, use the hull of the children to calculate its hull. - if self._node.callDecoration("isGroup"): - hull = Polygon(numpy.zeros((0, 2), dtype=numpy.int32)) - for child in self._node.getChildren(): - child_hull = child.callDecoration("getConvexHull") - if child_hull: - hull.setPoints(numpy.append(hull.getPoints(), child_hull.getPoints(), axis = 0)) - - if hull.getPoints().size < 3: - self._node.callDecoration("setConvexHull", None) - self._node.callDecoration("setConvexHullJob", None) - return - - Job.yieldThread() - - else: - if not self._node.getMeshData(): - return - mesh = self._node.getMeshData() - vertex_data = mesh.getTransformed(self._node.getWorldTransformation()).getVertices() - # Don't use data below 0. - # TODO; We need a better check for this as this gives poor results for meshes with long edges. - vertex_data = vertex_data[vertex_data[:,1] >= 0] - - # Round the vertex data to 1/10th of a mm, then remove all duplicate vertices - # This is done to greatly speed up further convex hull calculations as the convex hull - # becomes much less complex when dealing with highly detailed models. - vertex_data = numpy.round(vertex_data, 1) - - vertex_data = vertex_data[:, [0, 2]] # Drop the Y components to project to 2D. - - # Grab the set of unique points. - # - # This basically finds the unique rows in the array by treating them as opaque groups of bytes - # which are as long as the 2 float64s in each row, and giving this view to numpy.unique() to munch. - # See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array - vertex_byte_view = numpy.ascontiguousarray(vertex_data).view( - numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1]))) - _, idx = numpy.unique(vertex_byte_view, return_index=True) - vertex_data = vertex_data[idx] # Select the unique rows by index. - - hull = Polygon(vertex_data) - - # First, calculate the normal convex hull around the points - hull = hull.getConvexHull() - - # Then, do a Minkowski hull with a simple 1x1 quad to outset and round the normal convex hull. - # This is done because of rounding errors. - hull = hull.getMinkowskiHull(Polygon(numpy.array([[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]], numpy.float32))) - - ################################################################# - # Print Head Exclusion Zone - ################################################################# - - - # - # TODO - # ConvexHullDecorator should use a memoization strategy in its getters. - # Make MeshData immutable - - profile = Application.getInstance().getMachineManager().getWorkingProfile() - if profile: - if profile.getSettingValue("print_sequence") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"): - # Printing one at a time and it's not an object in a group - self._node.callDecoration("setConvexHullBoundary", copy.deepcopy(hull)) - head_and_fans = Polygon(numpy.array(profile.getSettingValue("machine_head_with_fans_polygon"), numpy.float32)) - - # Full head hull is used to actually check the order. - full_head_hull = hull.getMinkowskiHull(head_and_fans) - self._node.callDecoration("setConvexHullHeadFull", full_head_hull) - mirrored = copy.deepcopy(head_and_fans) - mirrored.mirror([0, 0], [0, 1]) #Mirror horizontally. - mirrored.mirror([0, 0], [1, 0]) #Mirror vertically. - head_and_fans = head_and_fans.intersectionConvexHulls(mirrored) - - # Min head hull is used for the push free - min_head_hull = hull.getMinkowskiHull(head_and_fans) - self._node.callDecoration("setConvexHullHead", min_head_hull) - hull = hull.getMinkowskiHull(Polygon(numpy.array(profile.getSettingValue("machine_head_polygon"),numpy.float32))) - else: - self._node.callDecoration("setConvexHullHead", None) - if self._node.getParent() is None: # Node was already deleted before job is done. - self._node.callDecoration("setConvexHullNode",None) - self._node.callDecoration("setConvexHull", None) - self._node.callDecoration("setConvexHullJob", None) - return - - hull_node = ConvexHullNode.ConvexHullNode(self._node, hull, Application.getInstance().getController().getScene().getRoot()) - self._node.callDecoration("setConvexHullNode", hull_node) - self._node.callDecoration("setConvexHull", hull) - self._node.callDecoration("setConvexHullJob", None) - - if self._node.getParent() and self._node.getParent().callDecoration("isGroup"): - job = self._node.getParent().callDecoration("getConvexHullJob") - if job: - job.cancel() - self._node.getParent().callDecoration("setConvexHull", None) - hull_node = self._node.getParent().callDecoration("getConvexHullNode") - if hull_node: - hull_node.setParent(None) - - try: - Logger.log('d', 'ConvexHullJob getConvexHull:' + dumpPoly(self._node.callDecoration("getConvexHull"))) - Logger.log('d', 'ConvexHullJob new getConvexHull:' + dumpPoly(self._node.callDecoration("newGetConvexHull"))) - except Exception: - pass - - try: - Logger.log('d', 'ConvexHullJob getConvexHullHeadFull:' + dumpPoly(self._node.callDecoration("getConvexHullHeadFull"))) - Logger.log('d', 'ConvexHullJob new getConvexHullHeadFull:' + dumpPoly(self._node.callDecoration("newGetConvexHullHeadFull"))) - except Exception: - pass - - try: - Logger.log('d', 'ConvexHullJob getConvexHullHead:' + dumpPoly(self._node.callDecoration("getConvexHullHead"))) - Logger.log('d', 'ConvexHullJob new getConvexHullHead:' + dumpPoly(self._node.callDecoration("newGetConvexHullHead"))) - except Exception: - pass - - try: - Logger.log('d', 'ConvexHullJob getConvexHullBoundary:' + dumpPoly(self._node.callDecoration("getConvexHullBoundary"))) - Logger.log('d', 'ConvexHullJob new getConvexHullBoundary:' + dumpPoly(self._node.callDecoration("newGetConvexHullBoundary"))) - except Exception: - pass - -def dumpPoly(poly): - if poly is None: - return "None" - else: - return repr(poly.getPoints()) From 5d533d642db6a905374a908ba7cb4f5c07d42f94 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Wed, 25 May 2016 15:47:12 +0200 Subject: [PATCH 13/18] PlatformPhysics doesn't need ConvexHullJob either. Contributes to CURA-1504 --- cura/PlatformPhysics.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index b0863ccfd7..c43d0d09d7 100644 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -14,7 +14,6 @@ from UM.Preferences import Preferences from cura.ConvexHullDecorator import ConvexHullDecorator from . import PlatformPhysicsOperation -from . import ConvexHullJob from . import ZOffsetDecorator import copy From 7e3dd3d443dfbda122bd14f0c2b73ba6f6831e13 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Wed, 25 May 2016 15:51:12 +0200 Subject: [PATCH 14/18] Removed some debug. Contributes to CURA-1504 --- cura/ConvexHullDecorator.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cura/ConvexHullDecorator.py b/cura/ConvexHullDecorator.py index 1d03c250e5..c42ab4f339 100644 --- a/cura/ConvexHullDecorator.py +++ b/cura/ConvexHullDecorator.py @@ -10,7 +10,7 @@ import numpy ## The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node. # If a scene node has a convex hull decorator, it will have a shadow in which other objects can not be printed. class ConvexHullDecorator(SceneNodeDecorator): - def __init__(self,): + def __init__(self): super().__init__() self._convex_hull_node = None @@ -65,10 +65,8 @@ class ConvexHullDecorator(SceneNodeDecorator): convex_hull = self.getConvexHull() if self._convex_hull_node: if self._convex_hull_node.getHull() == convex_hull: - Logger.log('d', 'ConvexHullDecorator not creating a new ConvexHullNode') return self._convex_hull_node.setParent(None) - Logger.log('d', 'ConvexHullDecorator creating ConvexHullNode') hull_node = ConvexHullNode.ConvexHullNode(self._node, convex_hull, Application.getInstance().getController().getScene().getRoot()) self._convex_hull_node = hull_node From 5f638f6e69b85d64d1904bac8c4c25417294e209 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Thu, 26 May 2016 14:39:22 +0200 Subject: [PATCH 15/18] Better handling of degenerate convex hull cases. Contributes to CURA-1504 --- cura/ConvexHullDecorator.py | 66 +++++++++++++++++++------------------ cura/ConvexHullNode.py | 14 ++++---- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/cura/ConvexHullDecorator.py b/cura/ConvexHullDecorator.py index c42ab4f339..2a8f55d09e 100644 --- a/cura/ConvexHullDecorator.py +++ b/cura/ConvexHullDecorator.py @@ -130,46 +130,48 @@ class ConvexHullDecorator(SceneNodeDecorator): return rounded_hull else: - if not self._node.getMeshData(): - return None - mesh = self._node.getMeshData() - world_transform = self._node.getWorldTransformation() + rounded_hull = None + if self._node.getMeshData(): + mesh = self._node.getMeshData() + world_transform = self._node.getWorldTransformation() - # Check the cache - if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform: - # Logger.log('d', 'Cache hit in _compute2DConvexHull mesh path') - return self._2d_convex_hull_mesh_result + # Check the cache + if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform: + # Logger.log('d', 'Cache hit in _compute2DConvexHull mesh path') + return self._2d_convex_hull_mesh_result - vertex_data = mesh.getConvexHullTransformedVertices(world_transform) - # Don't use data below 0. - # TODO; We need a better check for this as this gives poor results for meshes with long edges. - vertex_data = vertex_data[vertex_data[:,1] >= 0] + vertex_data = mesh.getConvexHullTransformedVertices(world_transform) + # Don't use data below 0. + # TODO; We need a better check for this as this gives poor results for meshes with long edges. + vertex_data = vertex_data[vertex_data[:,1] >= 0] - # Round the vertex data to 1/10th of a mm, then remove all duplicate vertices - # This is done to greatly speed up further convex hull calculations as the convex hull - # becomes much less complex when dealing with highly detailed models. - vertex_data = numpy.round(vertex_data, 1) + if len(vertex_data) >= 4: + # Round the vertex data to 1/10th of a mm, then remove all duplicate vertices + # This is done to greatly speed up further convex hull calculations as the convex hull + # becomes much less complex when dealing with highly detailed models. + vertex_data = numpy.round(vertex_data, 1) - vertex_data = vertex_data[:, [0, 2]] # Drop the Y components to project to 2D. + vertex_data = vertex_data[:, [0, 2]] # Drop the Y components to project to 2D. - # Grab the set of unique points. - # - # This basically finds the unique rows in the array by treating them as opaque groups of bytes - # which are as long as the 2 float64s in each row, and giving this view to numpy.unique() to munch. - # See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array - vertex_byte_view = numpy.ascontiguousarray(vertex_data).view( - numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1]))) - _, idx = numpy.unique(vertex_byte_view, return_index=True) - vertex_data = vertex_data[idx] # Select the unique rows by index. + # Grab the set of unique points. + # + # This basically finds the unique rows in the array by treating them as opaque groups of bytes + # which are as long as the 2 float64s in each row, and giving this view to numpy.unique() to munch. + # See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array + vertex_byte_view = numpy.ascontiguousarray(vertex_data).view( + numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1]))) + _, idx = numpy.unique(vertex_byte_view, return_index=True) + vertex_data = vertex_data[idx] # Select the unique rows by index. - hull = Polygon(vertex_data) + hull = Polygon(vertex_data) - # First, calculate the normal convex hull around the points - convex_hull = hull.getConvexHull() + if len(vertex_data) >= 4: + # First, calculate the normal convex hull around the points + convex_hull = hull.getConvexHull() - # Then, do a Minkowski hull with a simple 1x1 quad to outset and round the normal convex hull. - # This is done because of rounding errors. - rounded_hull = convex_hull.getMinkowskiHull(Polygon(numpy.array([[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]], numpy.float32))) + # Then, do a Minkowski hull with a simple 1x1 quad to outset and round the normal convex hull. + # This is done because of rounding errors. + rounded_hull = convex_hull.getMinkowskiHull(Polygon(numpy.array([[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]], numpy.float32))) # Store the result in the cache self._2d_convex_hull_mesh = mesh diff --git a/cura/ConvexHullNode.py b/cura/ConvexHullNode.py index 3d00edd288..2d88604b6e 100644 --- a/cura/ConvexHullNode.py +++ b/cura/ConvexHullNode.py @@ -39,9 +39,10 @@ class ConvexHullNode(SceneNode): self._convex_hull_head_mesh = None self._hull = hull - hull_mesh = self.createHullMesh(self._hull.getPoints()) - if hull_mesh: - self.setMeshData(hull_mesh) + if self._hull: + hull_mesh = self.createHullMesh(self._hull.getPoints()) + if hull_mesh: + self.setMeshData(hull_mesh) convex_hull_head = self._node.callDecoration("getConvexHullHead") if convex_hull_head: self._convex_hull_head_mesh = self.createHullMesh(convex_hull_head.getPoints()) @@ -76,9 +77,10 @@ class ConvexHullNode(SceneNode): self._shader.setUniformValue("u_color", self._color) if self.getParent(): - renderer.queueNode(self, transparent = True, shader = self._shader, backface_cull = True, sort = -8) - if self._convex_hull_head_mesh: - renderer.queueNode(self, shader = self._shader, transparent = True, mesh = self._convex_hull_head_mesh, backface_cull = True, sort = -8) + if self.getMeshData(): + renderer.queueNode(self, transparent = True, shader = self._shader, backface_cull = True, sort = -8) + if self._convex_hull_head_mesh: + renderer.queueNode(self, shader = self._shader, transparent = True, mesh = self._convex_hull_head_mesh, backface_cull = True, sort = -8) return True From 4cc0026af3cd1cf33f5ccb8a3cf6f4614037aea8 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Wed, 15 Jun 2016 16:22:48 +0200 Subject: [PATCH 16/18] Minor rework in response to code review. Contributes to CURA-1504 --- cura/ConvexHullDecorator.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cura/ConvexHullDecorator.py b/cura/ConvexHullDecorator.py index 2a8f55d09e..7efc8c7ffb 100644 --- a/cura/ConvexHullDecorator.py +++ b/cura/ConvexHullDecorator.py @@ -2,7 +2,6 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from UM.Application import Application from UM.Math.Polygon import Polygon -from UM.Logger import Logger from . import ConvexHullNode import numpy @@ -113,7 +112,6 @@ class ConvexHullDecorator(SceneNodeDecorator): # Check the cache if child_polygon == self._2d_convex_hull_group_child_polygon: - # Logger.log('d', 'Cache hit in _compute2DConvexHull group path') return self._2d_convex_hull_group_result # First, calculate the normal convex hull around the points @@ -137,7 +135,6 @@ class ConvexHullDecorator(SceneNodeDecorator): # Check the cache if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform: - # Logger.log('d', 'Cache hit in _compute2DConvexHull mesh path') return self._2d_convex_hull_mesh_result vertex_data = mesh.getConvexHullTransformedVertices(world_transform) From 9641a25f3136cab0c7ccde85b74fac5158654462 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Wed, 22 Jun 2016 14:48:15 +0200 Subject: [PATCH 17/18] Fixed up the convex hull 'shadow' creation and deletion after the merge. Contributes to CURA-1504 --- cura/ConvexHullDecorator.py | 32 ++++++++++++++++++++++++++++---- cura/ConvexHullNode.py | 14 -------------- cura/CuraApplication.py | 1 - 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/cura/ConvexHullDecorator.py b/cura/ConvexHullDecorator.py index fab22f898e..29fa3bce0a 100644 --- a/cura/ConvexHullDecorator.py +++ b/cura/ConvexHullDecorator.py @@ -19,6 +19,19 @@ class ConvexHullDecorator(SceneNodeDecorator): Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) self._onGlobalStackChanged() + def setNode(self, node): + previous_node = self._node + if previous_node is not None and node is not previous_node: + previous_node.transformationChanged.connect(self._onChanged) + previous_node.parentChanged.connect(self._onChanged) + + super().setNode(node) + + self._node.transformationChanged.connect(self._onChanged) + self._node.parentChanged.connect(self._onChanged) + + self._onChanged() + ## Force that a new (empty) object is created upon copy. def __deepcopy__(self, memo): return ConvexHullDecorator() @@ -67,16 +80,19 @@ class ConvexHullDecorator(SceneNodeDecorator): return None def recomputeConvexHull(self): - if self._node is None: - return None + root = Application.getInstance().getController().getScene().getRoot() + if self._node is None or not self.__isDescendant(root, self._node): + if self._convex_hull_node: + self._convex_hull_node.setParent(None) + self._convex_hull_node = None + return convex_hull = self.getConvexHull() if self._convex_hull_node: if self._convex_hull_node.getHull() == convex_hull: return self._convex_hull_node.setParent(None) - hull_node = ConvexHullNode.ConvexHullNode(self._node, convex_hull, - Application.getInstance().getController().getScene().getRoot()) + hull_node = ConvexHullNode.ConvexHullNode(self._node, convex_hull, root) self._convex_hull_node = hull_node def _onSettingValueChanged(self, key, property_name): @@ -205,3 +221,11 @@ class ConvexHullDecorator(SceneNodeDecorator): self._global_stack.containersChanged.connect(self._onChanged) self._onChanged() + + ## Returns true if node is a descendent or the same as the root node. + def __isDescendant(self, root, node): + if node is None: + return False + if root is node: + return True + return self.__isDescendant(root, node.getParent()) diff --git a/cura/ConvexHullNode.py b/cura/ConvexHullNode.py index 2d88604b6e..be571d111e 100644 --- a/cura/ConvexHullNode.py +++ b/cura/ConvexHullNode.py @@ -1,7 +1,6 @@ # Copyright (c) 2015 Ultimaker B.V. # Cura is released under the terms of the AGPLv3 or higher. -from UM.Application import Application from UM.Scene.SceneNode import SceneNode from UM.Resources import Resources from UM.Math.Color import Color @@ -31,8 +30,6 @@ class ConvexHullNode(SceneNode): # The node this mesh is "watching" self._node = node - self._node.transformationChanged.connect(self._onNodePositionChanged) - self._node.parentChanged.connect(self._onNodeParentChanged) self._node.decoratorsChanged.connect(self._onNodeDecoratorsChanged) self._onNodeDecoratorsChanged(self._node) @@ -84,17 +81,6 @@ class ConvexHullNode(SceneNode): return True - def _onNodePositionChanged(self, node): - if Application.getInstance().getController().isToolOperationActive(): - if node.callDecoration("getConvexHull"): - self.setParent(None) # Garbage collection should delete this node after a while. - - def _onNodeParentChanged(self, node): - if node.getParent(): - self.setParent(self._original_parent) - else: - self.setParent(None) - def _onNodeDecoratorsChanged(self, node): self._color = Color(35, 35, 35, 0.5) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index c9e7e701c4..12818e1e87 100644 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -741,7 +741,6 @@ class CuraApplication(QtApplication): # Add all individual nodes to the selection Selection.add(child) - child.callDecoration("setConvexHull", None) op.push() # Note: The group removes itself from the scene once all its children have left it, From 631ef8e6784f38aa8948809eae4ba369e438e1c9 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Wed, 22 Jun 2016 15:06:03 +0200 Subject: [PATCH 18/18] Reimplement the "no convex hull when using a tool" functionality. Contributes to CURA-1504 --- cura/ConvexHullDecorator.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cura/ConvexHullDecorator.py b/cura/ConvexHullDecorator.py index 29fa3bce0a..9fccd46988 100644 --- a/cura/ConvexHullDecorator.py +++ b/cura/ConvexHullDecorator.py @@ -17,6 +17,9 @@ class ConvexHullDecorator(SceneNodeDecorator): self._global_stack = None Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) + Application.getInstance().getController().toolOperationStarted.connect(self._onChanged) + Application.getInstance().getController().toolOperationStopped.connect(self._onChanged) + self._onGlobalStackChanged() def setNode(self, node): @@ -80,8 +83,9 @@ class ConvexHullDecorator(SceneNodeDecorator): return None def recomputeConvexHull(self): - root = Application.getInstance().getController().getScene().getRoot() - if self._node is None or not self.__isDescendant(root, self._node): + controller = Application.getInstance().getController() + root = controller.getScene().getRoot() + if self._node is None or controller.isToolOperationActive() or not self.__isDescendant(root, self._node): if self._convex_hull_node: self._convex_hull_node.setParent(None) self._convex_hull_node = None