def read(self, file_name): result = [] # The base object of 3mf is a zipped archive. archive = zipfile.ZipFile(file_name, "r") self._base_name = os.path.basename(file_name) try: self._root = ET.parse(archive.open("3D/3dmodel.model")) self._unit = self._root.getroot().get("unit") build_items = self._root.findall("./3mf:build/3mf:item", self._namespaces) for build_item in build_items: id = build_item.get("objectid") object = self._root.find("./3mf:resources/3mf:object[@id='{0}']".format(id), self._namespaces) if "type" in object.attrib: if object.attrib["type"] == "support" or object.attrib["type"] == "other": # Ignore support objects, as cura does not support these. # We can't guarantee that they wont be made solid. # We also ignore "other", as I have no idea what to do with them. Logger.log("w", "3MF file contained an object of type %s which is not supported by Cura", object.attrib["type"]) continue elif object.attrib["type"] == "solidsupport" or object.attrib["type"] == "model": pass # Load these as normal else: # We should technically fail at this point because it's an invalid 3MF, but try to continue anyway. Logger.log("e", "3MF file contained an object of type %s which is not supported by the 3mf spec", object.attrib["type"]) continue build_item_node = self._createNodeFromObject(object, self._base_name + "_" + str(id)) # compensate for original center position, if object(s) is/are not around its zero position extents = build_item_node.getMeshData().getExtents() center_vector = Vector(extents.center.x, extents.center.y, extents.center.z) transform_matrix = Matrix() transform_matrix.setByTranslation(center_vector) # offset with transform from 3mf transform = build_item.get("transform") if transform is not None: transform_matrix.multiply(self._createMatrixFromTransformationString(transform)) build_item_node.setTransformation(transform_matrix) global_container_stack = UM.Application.getInstance().getGlobalContainerStack() # Create a transformation Matrix to convert from 3mf worldspace into ours. # First step: flip the y and z axis. transformation_matrix = Matrix() transformation_matrix._data[1, 1] = 0 transformation_matrix._data[1, 2] = 1 transformation_matrix._data[2, 1] = -1 transformation_matrix._data[2, 2] = 0 # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the # build volume. if global_container_stack: translation_vector = Vector(x = -global_container_stack.getProperty("machine_width", "value") / 2, y = -global_container_stack.getProperty("machine_depth", "value") / 2, z = 0) translation_matrix = Matrix() translation_matrix.setByTranslation(translation_vector) transformation_matrix.multiply(translation_matrix) # Third step: 3MF also defines a unit, wheras Cura always assumes mm. scale_matrix = Matrix() scale_matrix.setByScaleVector(self._getScaleFromUnit(self._unit)) transformation_matrix.multiply(scale_matrix) # Pre multiply the transformation with the loaded transformation, so the data is handled correctly. build_item_node.setTransformation(build_item_node.getLocalTransformation().preMultiply(transformation_matrix)) result.append(build_item_node) except Exception as e: Logger.log("e", "An exception occurred in 3mf reader: %s", e) return result
def getScale(self) -> Vector: x = numpy.linalg.norm(self._data[0, 0:3]) y = numpy.linalg.norm(self._data[1, 0:3]) z = numpy.linalg.norm(self._data[2, 0:3]) return Vector(x, y, z)
def run(self): status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"), lifetime = 0, dismissable=False, progress = 0, title = i18n_catalog.i18nc("@info:title", "Finding Location")) status_message.show() arranger = Arrange.create(fixed_nodes = self._fixed_nodes) # Collect nodes to be placed nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr) for node in self._nodes: offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset) nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr)) # Sort the nodes with the biggest area first. nodes_arr.sort(key=lambda item: item[0]) nodes_arr.reverse() # Place nodes one at a time start_priority = 0 last_priority = start_priority last_size = None grouped_operation = GroupedOperation() found_solution_for_all = True for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr): # For performance reasons, we assume that when a location does not fit, # it will also not fit for the next object (while what can be untrue). # We also skip possibilities by slicing through the possibilities (step = 10) if last_size == size: # This optimization works if many of the objects have the same size start_priority = last_priority else: start_priority = 0 best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority, step=10) x, y = best_spot.x, best_spot.y node.removeDecorator(ZOffsetDecorator) if node.getBoundingBox(): center_y = node.getWorldPosition().y - node.getBoundingBox().bottom else: center_y = 0 if x is not None: # We could find a place last_size = size last_priority = best_spot.priority arranger.place(x, y, hull_shape_arr) # take place before the next one grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True)) else: Logger.log("d", "Arrange all: could not find spot!") found_solution_for_all = False grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, - idx * 20), set_position = True)) status_message.setProgress((idx + 1) / len(nodes_arr) * 100) Job.yieldThread() grouped_operation.push() status_message.hide() if not found_solution_for_all: no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), title = i18n_catalog.i18nc("@info:title", "Can't Find Location")) no_full_solution_message.show()
def _getNullBoundingBox(): return AxisAlignedBox(minimum=Vector(0, 0, 0), maximum=Vector(10, 10, 10))
def _renderItem(self, item): node = item["node"] mesh = item.get("mesh", node.getMeshData()) if not mesh: return #Something went wrong, node has no mesh. transform = node.getWorldTransformation() material = item["material"] mode = item["mode"] wireframe = item.get("wireframe", False) range = item.get("range", None) culling_enabled = self._gl.glIsEnabled(self._gl.GL_CULL_FACE) if item.get("force_single_sided") and not culling_enabled: self._gl.glEnable(self._gl.GL_CULL_FACE) material.bind() material.setUniformValue("u_projectionMatrix", self._camera.getProjectionMatrix(), cache=False) material.setUniformValue( "u_viewMatrix", self._camera.getWorldTransformation().getInverse(), cache=False) material.setUniformValue("u_viewPosition", self._camera.getWorldPosition(), cache=False) material.setUniformValue("u_modelMatrix", transform, cache=False) material.setUniformValue("u_lightPosition", self._camera.getWorldPosition() + Vector(0, 50, 0), cache=False) if mesh.hasNormals(): normal_matrix = copy.deepcopy(transform) normal_matrix.setRow(3, [0, 0, 0, 1]) normal_matrix.setColumn(3, [0, 0, 0, 1]) normal_matrix = normal_matrix.getInverse().getTransposed() material.setUniformValue("u_normalMatrix", normal_matrix, cache=False) vertex_buffer = None try: vertex_buffer = getattr(mesh, vertexBufferProperty) except AttributeError: pass if vertex_buffer is None: vertex_buffer = self._createVertexBuffer(mesh) vertex_buffer.bind() if mesh.hasIndices(): index_buffer = None try: index_buffer = getattr(mesh, indexBufferProperty) except AttributeError: pass if index_buffer is None: index_buffer = self._createIndexBuffer(mesh) index_buffer.bind() material.enableAttribute("a_vertex", "vector3f", 0) offset = mesh.getVertexCount() * 3 * 4 if mesh.hasNormals(): material.enableAttribute("a_normal", "vector3f", offset) offset += mesh.getVertexCount() * 3 * 4 if mesh.hasColors(): material.enableAttribute("a_color", "vector4f", offset) offset += mesh.getVertexCount() * 4 * 4 if mesh.hasUVCoordinates(): material.enableAttribute("a_uvs", "vector2f", offset) offset += mesh.getVertexCount() * 2 * 4 if wireframe and hasattr(self._gl, "glPolygonMode"): self._gl.glPolygonMode(self._gl.GL_FRONT_AND_BACK, self._gl.GL_LINE) if mesh.hasIndices(): if range is None: if mode == self._gl.GL_TRIANGLES: self._gl.glDrawElements(mode, mesh.getFaceCount() * 3, self._gl.GL_UNSIGNED_INT, None) else: self._gl.glDrawElements(mode, mesh.getFaceCount(), self._gl.GL_UNSIGNED_INT, None) else: if mode == self._gl.GL_TRIANGLES: self._gl.glDrawRangeElements(mode, range[0], range[1], range[1] - range[0], self._gl.GL_UNSIGNED_INT, None) else: self._gl.glDrawRangeElements(mode, range[0], range[1], range[1] - range[0], self._gl.GL_UNSIGNED_INT, None) else: self._gl.glDrawArrays(mode, 0, mesh.getVertexCount()) if wireframe and hasattr(self._gl, "glPolygonMode"): self._gl.glPolygonMode(self._gl.GL_FRONT_AND_BACK, self._gl.GL_FILL) material.disableAttribute("a_vertex") material.disableAttribute("a_normal") material.disableAttribute("a_color") material.disableAttribute("a_uvs") vertex_buffer.release() if mesh.hasIndices(): index_buffer.release() material.release() if item.get("force_single_sided") and not culling_enabled: self._gl.glDisable(self._gl.GL_CULL_FACE)
def updateHeadPosition(self, x: float, y: float, z: float) -> None: if self._head_position.x != x or self._head_position.y != y or self._head_position.z != z: self._head_position = Vector(x, y, z) self.headPositionChanged.emit()
def _onChangeTimerFinished(self, was_triggered_by_tool=False): if not self._enabled: return root = self._controller.getScene().getRoot() # Keep a list of nodes that are moving. We use this so that we don't move two intersecting objects in the # same direction. transformed_nodes = [] # We try to shuffle all the nodes to prevent "locked" situations, where iteration B inverts iteration A. # By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve. nodes = list(BreadthFirstIterator(root)) # Only check nodes inside build area. nodes = [ node for node in nodes if (hasattr(node, "_outside_buildarea") and not node._outside_buildarea) ] random.shuffle(nodes) for node in nodes: if node is root or type( node) is not SceneNode or node.getBoundingBox() is None: continue bbox = node.getBoundingBox() # Move it downwards if bottom is above platform move_vector = Vector() if Preferences.getInstance().getValue( "physics/automatic_drop_down") and not ( node.getParent() and node.getParent().callDecoration( "isGroup")) and node.isEnabled( ): #If an object is grouped, don't move it down z_offset = node.callDecoration( "getZOffset") if node.getDecorator( ZOffsetDecorator.ZOffsetDecorator) else 0 move_vector = move_vector.set(y=-bbox.bottom + z_offset) # If there is no convex hull for the node, start calculating it and continue. if not node.getDecorator(ConvexHullDecorator): node.addDecorator(ConvexHullDecorator()) 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. if other_node is root or type( other_node) is not SceneNode or other_node is node: continue # Ignore collisions of a group with it's own children if other_node in node.getAllChildren( ) or node in other_node.getAllChildren(): continue # Ignore collisions within a group if other_node.getParent() and node.getParent() and ( other_node.getParent().callDecoration("isGroup") is not None or node.getParent().callDecoration("isGroup") is not None): continue # Ignore nodes that do not have the right properties set. if not other_node.callDecoration( "getConvexHull") or not other_node.getBoundingBox( ): continue if other_node in transformed_nodes: continue # Other node is already moving, wait for next pass. overlap = (0, 0) # Start loop with no overlap current_overlap_checks = 0 # Continue to check the overlap until we no longer find one. while overlap and current_overlap_checks < self._max_overlap_checks: current_overlap_checks += 1 head_hull = node.callDecoration("getConvexHullHead") if head_hull: # One at a time intersection. overlap = head_hull.translate( move_vector.x, move_vector.z).intersectsPolygon( other_node.callDecoration("getConvexHull")) if not overlap: other_head_hull = other_node.callDecoration( "getConvexHullHead") if other_head_hull: overlap = node.callDecoration( "getConvexHull").translate( move_vector.x, move_vector.z).intersectsPolygon( other_head_hull) if overlap: # Moving ensured that overlap was still there. Try anew! move_vector = move_vector.set( x=move_vector.x + overlap[0] * self._move_factor, z=move_vector.z + overlap[1] * self._move_factor) else: # Moving ensured that overlap was still there. Try anew! move_vector = move_vector.set( x=move_vector.x + overlap[0] * self._move_factor, z=move_vector.z + overlap[1] * self._move_factor) else: own_convex_hull = node.callDecoration( "getConvexHull") other_convex_hull = other_node.callDecoration( "getConvexHull") if own_convex_hull and other_convex_hull: overlap = own_convex_hull.translate( move_vector.x, move_vector.z).intersectsPolygon( other_convex_hull) if overlap: # Moving ensured that overlap was still there. Try anew! move_vector = move_vector.set( x=move_vector.x + overlap[0] * self._move_factor, z=move_vector.z + overlap[1] * self._move_factor) else: # This can happen in some cases if the object is not yet done with being loaded. # Simply waiting for the next tick seems to resolve this correctly. overlap = None if not Vector.Null.equals(move_vector, epsilon=1e-5): transformed_nodes.append(node) op = PlatformPhysicsOperation.PlatformPhysicsOperation( node, move_vector) op.push() # After moving, we have to evaluate the boundary checks for nodes build_volume = Application.getInstance().getBuildVolume() build_volume.updateNodeBoundaryCheck()
def readVector(node, attr, default): v = readFloatArray(node, attr, default) return Vector(v[0], v[1], v[2])
def readRotation(node, attr, default): v = readFloatArray(node, attr, default) return (v[3], Vector(v[0], v[1], v[2]))
def _generateSceneNode(self, file_name, xz_size, peak_height, base_height, blur_iterations, max_size, image_color_invert): scene_node = SceneNode() mesh = MeshBuilder() img = QImage(file_name) if img.isNull(): Logger.log("e", "Image is corrupt.") return None width = max(img.width(), 2) height = max(img.height(), 2) aspect = height / width if img.width() < 2 or img.height() < 2: img = img.scaled(width, height, Qt.IgnoreAspectRatio) base_height = max(base_height, 0) peak_height = max(peak_height, -base_height) xz_size = max(xz_size, 1) scale_vector = Vector(xz_size, peak_height, xz_size) if width > height: scale_vector = scale_vector.set(z=scale_vector.z * aspect) elif height > width: scale_vector = scale_vector.set(x=scale_vector.x / aspect) if width > max_size or height > max_size: scale_factor = max_size / width if height > width: scale_factor = max_size / height width = int(max(round(width * scale_factor), 2)) height = int(max(round(height * scale_factor), 2)) img = img.scaled(width, height, Qt.IgnoreAspectRatio) width_minus_one = width - 1 height_minus_one = height - 1 Job.yieldThread() texel_width = 1.0 / (width_minus_one) * scale_vector.x texel_height = 1.0 / (height_minus_one) * scale_vector.z height_data = numpy.zeros((height, width), dtype=numpy.float32) for x in range(0, width): for y in range(0, height): qrgb = img.pixel(x, y) avg = float(qRed(qrgb) + qGreen(qrgb) + qBlue(qrgb)) / (3 * 255) height_data[y, x] = avg Job.yieldThread() if image_color_invert: height_data = 1 - height_data for _ in range(0, blur_iterations): copy = numpy.pad(height_data, ((1, 1), (1, 1)), mode="edge") height_data += copy[1:-1, 2:] height_data += copy[1:-1, :-2] height_data += copy[2:, 1:-1] height_data += copy[:-2, 1:-1] height_data += copy[2:, 2:] height_data += copy[:-2, 2:] height_data += copy[2:, :-2] height_data += copy[:-2, :-2] height_data /= 9 Job.yieldThread() height_data *= scale_vector.y height_data += base_height heightmap_face_count = 2 * height_minus_one * width_minus_one total_face_count = heightmap_face_count + (width_minus_one * 2) * ( height_minus_one * 2) + 2 mesh.reserveFaceCount(total_face_count) # initialize to texel space vertex offsets. # 6 is for 6 vertices for each texel quad. heightmap_vertices = numpy.zeros( (width_minus_one * height_minus_one, 6, 3), dtype=numpy.float32) heightmap_vertices = heightmap_vertices + numpy.array( [[[0, base_height, 0], [0, base_height, texel_height], [texel_width, base_height, texel_height], [texel_width, base_height, texel_height], [texel_width, base_height, 0], [0, base_height, 0]]], dtype=numpy.float32) offsetsz, offsetsx = numpy.mgrid[0:height_minus_one, 0:width - 1] offsetsx = numpy.array(offsetsx, numpy.float32).reshape( -1, 1) * texel_width offsetsz = numpy.array(offsetsz, numpy.float32).reshape( -1, 1) * texel_height # offsets for each texel quad heightmap_vertex_offsets = numpy.concatenate([ offsetsx, numpy.zeros((offsetsx.shape[0], offsetsx.shape[1]), dtype=numpy.float32), offsetsz ], 1) heightmap_vertices += heightmap_vertex_offsets.repeat(6, 0).reshape( -1, 6, 3) # apply height data to y values heightmap_vertices[:, 0, 1] = heightmap_vertices[:, 5, 1] = height_data[:-1, : -1].reshape( -1) heightmap_vertices[:, 1, 1] = height_data[1:, :-1].reshape(-1) heightmap_vertices[:, 2, 1] = heightmap_vertices[:, 3, 1] = height_data[ 1:, 1:].reshape(-1) heightmap_vertices[:, 4, 1] = height_data[:-1, 1:].reshape(-1) heightmap_indices = numpy.array(numpy.mgrid[0:heightmap_face_count * 3], dtype=numpy.int32).reshape(-1, 3) mesh._vertices[0:(heightmap_vertices.size // 3), :] = heightmap_vertices.reshape(-1, 3) mesh._indices[0:(heightmap_indices.size // 3), :] = heightmap_indices mesh._vertex_count = heightmap_vertices.size // 3 mesh._face_count = heightmap_indices.size // 3 geo_width = width_minus_one * texel_width geo_height = height_minus_one * texel_height # bottom 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): x = n * texel_width nx = (n + 1) * texel_width hn0 = height_data[0, n] hn1 = height_data[0, n + 1] hs0 = height_data[height_minus_one, n] hs1 = height_data[height_minus_one, n + 1] mesh.addFaceByPoints(x, 0, 0, nx, 0, 0, nx, hn1, 0) mesh.addFaceByPoints(nx, hn1, 0, x, hn0, 0, x, 0, 0) 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): y = n * texel_height ny = (n + 1) * texel_height hw0 = height_data[n, 0] hw1 = height_data[n + 1, 0] he0 = height_data[n, width_minus_one] he1 = height_data[n + 1, width_minus_one] mesh.addFaceByPoints(0, 0, y, 0, 0, ny, 0, hw1, ny) mesh.addFaceByPoints(0, hw1, ny, 0, hw0, y, 0, 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
def processGeometryExtrusion(self, node): ccw = readBoolean(node, "ccw", True) begin_cap = readBoolean(node, "beginCap", True) end_cap = readBoolean(node, "endCap", True) cross = readFloatArray(node, "crossSection", (1, 1, 1, -1, -1, -1, -1, 1, 1, 1)) cross = [(cross[i], cross[i+1]) for i in range(0, len(cross), 2)] spine = readFloatArray(node, "spine", (0, 0, 0, 0, 1, 0)) spine = [(spine[i], spine[i+1], spine[i+2]) for i in range(0, len(spine), 3)] orient = readFloatArray(node, "orientation", None) if orient: # This converts X3D's axis/angle rotation to a 3x3 numpy matrix def toRotationMatrix(rot): (x, y, z) = rot[:3] a = rot[3] s = sin(a) c = cos(a) t = 1-c return numpy.array(( (x * x * t + c, x * y * t - z*s, x * z * t + y * s), (x * y * t + z*s, y * y * t + c, y * z * t - x * s), (x * z * t - y * s, y * z * t + x * s, z * z * t + c))) orient = [toRotationMatrix(orient[i:i+4]) if orient[i+3] != 0 else None for i in range(0, len(orient), 4)] scale = readFloatArray(node, "scale", None) if scale: scale = [numpy.array(((scale[i], 0, 0), (0, 1, 0), (0, 0, scale[i+1]))) if scale[i] != 1 or scale[i+1] != 1 else None for i in range(0, len(scale), 2)] # Special treatment for the closed spine and cross section. # Let's save some memory by not creating identical but distinct vertices; # later we'll introduce conditional logic to link the last vertex with # the first one where necessary. crossClosed = cross[0] == cross[-1] if crossClosed: cross = cross[:-1] nc = len(cross) cross = [numpy.array((c[0], 0, c[1])) for c in cross] ncf = nc if crossClosed else nc - 1 # Face count along the cross; for closed cross, it's the same as the # respective vertex count spine_closed = spine[0] == spine[-1] if spine_closed: spine = spine[:-1] ns = len(spine) spine = [Vector(*s) for s in spine] nsf = ns if spine_closed else ns - 1 # This will be used for fallback, where the current spine point joins # two collinear spine segments. No need to recheck the case of the # closed spine/last-to-first point juncture; if there's an angle there, # it would kick in on the first iteration of the main loop by spine. def findFirstAngleNormal(): for i in range(1, ns - 1): spt = spine[i] z = (spine[i + 1] - spt).cross(spine[i - 1] - spt) if z.length() > EPSILON: return z # All the spines are collinear. Fallback to the rotated source # XZ plane. # TODO: handle the situation where the first two spine points match if len(spine) < 2: return Vector(0, 0, 1) v = spine[1] - spine[0] orig_y = Vector(0, 1, 0) orig_z = Vector(0, 0, 1) if v.cross(orig_y).length() > EPSILON: # Spine at angle with global y - rotate the z accordingly a = v.cross(orig_y) # Axis of rotation to get to the Z (x, y, z) = a.normalized().getData() s = a.length()/v.length() c = sqrt(1-s*s) t = 1-c m = numpy.array(( (x * x * t + c, x * y * t + z*s, x * z * t - y * s), (x * y * t - z*s, y * y * t + c, y * z * t + x * s), (x * z * t + y * s, y * z * t - x * s, z * z * t + c))) orig_z = Vector(*m.dot(orig_z.getData())) return orig_z self.reserveFaceAndVertexCount(2*nsf*ncf + (nc - 2 if begin_cap else 0) + (nc - 2 if end_cap else 0), ns*nc) z = None for i, spt in enumerate(spine): if (i > 0 and i < ns - 1) or spine_closed: snext = spine[(i + 1) % ns] sprev = spine[(i - 1 + ns) % ns] y = snext - sprev vnext = snext - spt vprev = sprev - spt try_z = vnext.cross(vprev) # Might be zero, then all kinds of fallback if try_z.length() > EPSILON: if z is not None and try_z.dot(z) < 0: try_z = -try_z z = try_z elif not z: # No z, and no previous z. # Look ahead, see if there's at least one point where # spines are not collinear. z = findFirstAngleNormal() elif i == 0: # And non-crossed snext = spine[i + 1] y = snext - spt z = findFirstAngleNormal() else: # last point and not crossed sprev = spine[i - 1] y = spt - sprev # If there's more than one point in the spine, z is already set. # One point in the spline is an error anyway. z = z.normalized() y = y.normalized() x = y.cross(z) # Already normalized m = numpy.array(((x.x, y.x, z.x), (x.y, y.y, z.y), (x.z, y.z, z.z))) # Columns are the unit vectors for the xz plane for the cross-section if orient: mrot = orient[i] if len(orient) > 1 else orient[0] if not mrot is None: m = m.dot(mrot) # Tested against X3DOM, the result matches, still not sure :( if scale: mscale = scale[i] if len(scale) > 1 else scale[0] if not mscale is None: m = m.dot(mscale) # First the cross-section 2-vector is scaled, # then rotated (which may make it a 3-vector), # then applied to the xz plane unit vectors sptv3 = numpy.array(spt.getData()[:3]) for cpt in cross: v = sptv3 + m.dot(cpt) self.addVertex(*v) if begin_cap: self.addFace([x for x in range(nc - 1, -1, -1)], ccw) # Order of edges in the face: forward along cross, forward along spine, # backward along cross, backward along spine, flipped if now ccw. # This order is assumed later in the texture coordinate assignment; # please don't change without syncing. for s in range(ns - 1): for c in range(ncf): self.addQuadFlip(s * nc + c, s * nc + (c + 1) % nc, (s + 1) * nc + (c + 1) % nc, (s + 1) * nc + c, ccw) if spine_closed: # The faces between the last and the first spine points b = (ns - 1) * nc for c in range(ncf): self.addQuadFlip(b + c, b + (c + 1) % nc, (c + 1) % nc, c, ccw) if end_cap: self.addFace([(ns - 1) * nc + x for x in range(0, nc)], ccw)
def homeCamera(self) -> None: scene = Application.getInstance().getController().getScene() camera = scene.getActiveCamera() camera.setPosition(Vector(-80, 250, 700)) camera.setPerspective(True) camera.lookAt(Vector(0, 0, 0))
def updateCurrentValue(self, value): self._camera_tool.setOrigin(Vector(value.x(), value.y(), value.z()))
def event(self, event): super().event(event) if event.type == Event.KeyPressEvent and event.key == KeyEvent.ShiftKey: # Snap is toggled when pressing the shift button self.setRotationSnap(not self._snap_rotation) if event.type == Event.KeyReleaseEvent and event.key == KeyEvent.ShiftKey: # Snap is "toggled back" when releasing the shift button self.setRotationSnap(not self._snap_rotation) if event.type == Event.MousePressEvent and self._controller.getToolsEnabled( ): # Start a rotate operation if MouseEvent.LeftButton not in event.buttons: return False id = self._selection_pass.getIdAtPosition(event.x, event.y) if not id: return False if self._handle.isAxis(id): self.setLockedAxis(id) else: # Not clicked on an axis: do nothing. return False handle_position = self._handle.getWorldPosition() # Save the current positions of the node, as we want to rotate around their current centres self._saved_node_positions = [] for node in self._getSelectedObjectsWithoutSelectedAncestors(): self._saved_node_positions.append((node, node.getPosition())) if id == ToolHandle.XAxis: self.setDragPlane(Plane(Vector(1, 0, 0), handle_position.x)) elif id == ToolHandle.YAxis: self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y)) elif self._locked_axis == ToolHandle.ZAxis: self.setDragPlane(Plane(Vector(0, 0, 1), handle_position.z)) else: self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y)) self.setDragStart(event.x, event.y) self._rotating = False self._angle = 0 return True if event.type == Event.MouseMoveEvent: # Perform a rotate operation if not self.getDragPlane(): return False if not self.getDragStart(): self.setDragStart(event.x, event.y) if not self.getDragStart(): #May have set it to None. return False if not self._rotating: self._rotating = True self.operationStarted.emit(self) handle_position = self._handle.getWorldPosition() drag_start = (self.getDragStart() - handle_position).normalized() drag_position = self.getDragPosition(event.x, event.y) if not drag_position: return False drag_end = (drag_position - handle_position).normalized() try: angle = math.acos(drag_start.dot(drag_end)) except ValueError: angle = 0 if self._snap_rotation: angle = int(angle / self._snap_angle) * self._snap_angle if angle == 0: return False rotation = None if self.getLockedAxis() == ToolHandle.XAxis: direction = 1 if Vector.Unit_X.dot( drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_X) elif self.getLockedAxis() == ToolHandle.YAxis: direction = 1 if Vector.Unit_Y.dot( drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_Y) elif self.getLockedAxis() == ToolHandle.ZAxis: direction = 1 if Vector.Unit_Z.dot( drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_Z) else: direction = -1 # Rate-limit the angle change notification # This is done to prevent the UI from being flooded with property change notifications, # which in turn would trigger constant repaints. new_time = time.monotonic() if not self._angle_update_time or new_time - self._angle_update_time > 0.1: self._angle_update_time = new_time self._angle += direction * angle self.propertyChanged.emit() # Rotate around the saved centeres of all selected nodes if len(self._saved_node_positions) > 1: op = GroupedOperation() for node, position in self._saved_node_positions: op.addOperation( RotateOperation(node, rotation, rotate_around_point=position)) op.push() else: for node, position in self._saved_node_positions: RotateOperation(node, rotation, rotate_around_point=position).push() self.setDragStart(event.x, event.y) return True if event.type == Event.MouseReleaseEvent: # Finish a rotate operation if self.getDragPlane(): self.setDragPlane(None) self.setLockedAxis(ToolHandle.NoAxis) self._angle = None self.propertyChanged.emit() if self._rotating: self.operationStopped.emit(self) return True
def setCameraRotation(self, coordinate: str = "x", angle: int = 0) -> None: camera = self._scene.getActiveCamera() if not camera: return camera.setZoomFactor(camera.getDefaultZoomFactor()) self._camera_tool.setOrigin(Vector(0, 100, 0)) # type: ignore if coordinate == "home": camera.setPosition(Vector(0, 100, 700)) camera.lookAt(Vector(0, 100, 0)) self._camera_tool.rotateCamera(0, 0) # type: ignore elif coordinate == "3d": camera.setPosition(Vector(-750, 600, 700)) camera.lookAt(Vector(0, 100, 100)) self._camera_tool.rotateCamera(0, 0) # type: ignore else: # for comparison is == used, because might not store them at the same location # https://stackoverflow.com/questions/1504717/why-does-comparing-strings-in-python-using-either-or-is-sometimes-produce if coordinate == "x": camera.setPosition(Vector(0, 100, 700)) camera.lookAt(Vector(0, 100, 0)) self._camera_tool.rotateCamera(angle, 0) # type: ignore elif coordinate == "y": if angle == 90: # Prepare the camera for top view, so no rotation has to be applied after setting the top view. camera.setPosition(Vector(0, 100, 100)) camera.lookAt(Vector(0, 100, 0)) self._camera_tool.rotateCamera(90, 0) # type: ignore # Actually set the top view. camera.setPosition(Vector(0, 800, 1)) camera.lookAt(Vector(0, 100, 1)) self._camera_tool.rotateCamera(0, 0) # type: ignore else: camera.setPosition(Vector(0, 100, 700)) camera.lookAt(Vector(0, 100, 0)) self._camera_tool.rotateCamera(0, angle) # type: ignore
def event(self, event): super().event(event) # Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes if event.type == Event.ToolActivateEvent: for node in Selection.getAllSelectedObjects(): node.boundingBoxChanged.connect(self.propertyChanged) if event.type == Event.ToolDeactivateEvent: for node in Selection.getAllSelectedObjects(): node.boundingBoxChanged.disconnect(self.propertyChanged) if event.type == Event.KeyPressEvent and event.key == KeyEvent.ShiftKey: return False if event.type == Event.MousePressEvent and self._controller.getToolsEnabled( ): # Start a translate operation if MouseEvent.LeftButton not in event.buttons: return False id = self._selection_pass.getIdAtPosition(event.x, event.y) if not id: return False if id in self._enabled_axis: self.setLockedAxis(id) elif self._handle.isAxis(id): return False self._moved = False if id == ToolHandle.XAxis: self.setDragPlane(Plane(Vector(0, 0, 1), 0)) elif id == ToolHandle.YAxis: self.setDragPlane(Plane(Vector(0, 0, 1), 0)) elif id == ToolHandle.ZAxis: self.setDragPlane(Plane(Vector(0, 1, 0), 0)) else: self.setDragPlane(Plane(Vector(0, 1, 0), 0)) if event.type == Event.MouseMoveEvent: # Perform a translate operation if not self.getDragPlane(): return False if not self.getDragStart(): self.setDragStart(event.x, event.y) return False drag = self.getDragVector(event.x, event.y) if drag: if self._grid_snap and drag.length() < self._grid_size: return False if self.getLockedAxis() == ToolHandle.XAxis: drag = drag.set(y=0, z=0) elif self.getLockedAxis() == ToolHandle.YAxis: drag = drag.set(x=0, z=0) elif self.getLockedAxis() == ToolHandle.ZAxis: drag = drag.set(x=0, y=0) if not self._moved: self._moved = True self._distance = Vector(0, 0, 0) self.operationStarted.emit(self) op = GroupedOperation() for node in Selection.getAllSelectedObjects(): op.addOperation(TranslateOperation(node, drag)) op.push() self._distance += drag self.setDragStart(event.x, event.y) # Rate-limit the angle change notification # This is done to prevent the UI from being flooded with property change notifications, # which in turn would trigger constant repaints. new_time = time.monotonic() if not self._distance_update_time or new_time - self._distance_update_time > 0.1: self.propertyChanged.emit() self._distance_update_time = new_time return True if event.type == Event.MouseReleaseEvent: # Finish a translate operation if self.getDragPlane(): self.operationStopped.emit(self) self._distance = None self.propertyChanged.emit() self.setLockedAxis(None) self.setDragPlane(None) self.setDragStart(None, None) return True return False
def run(self): Logger.log( "d", "Processing new layer for build plate %s..." % self._build_plate_number) start_time = time() view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "SimulationView": view.resetLayerData() self._progress_message.show() Job.yieldThread() if self._abort_requested: if self._progress_message: self._progress_message.hide() return Application.getInstance().getController().activeViewChanged.connect( self._onActiveViewChanged) # The no_setting_override is here because adding the SettingOverrideDecorator will trigger a reslice new_node = CuraSceneNode(no_setting_override=True) new_node.addDecorator(BuildPlateDecorator(self._build_plate_number)) # Force garbage collection. # For some reason, Python has a tendency to keep the layer data # in memory longer than needed. Forcing the GC to run here makes # sure any old layer data is really cleaned up before adding new. gc.collect() mesh = MeshData() layer_data = LayerDataBuilder.LayerDataBuilder() layer_count = len(self._layers) # Find the minimum layer number # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we # instead simply offset all other layers so the lowest layer is always 0. It could happens that # the first raft layer has value -8 but there are just 4 raft (negative) layers. min_layer_number = 0 negative_layers = 0 for layer in self._layers: if layer.id < min_layer_number: min_layer_number = layer.id if layer.id < 0: negative_layers += 1 current_layer = 0 for layer in self._layers: # Negative layers are offset by the minimum layer number, but the positive layers are just # offset by the number of negative layers so there is no layer gap between raft and model abs_layer_number = layer.id + abs( min_layer_number ) if layer.id < 0 else layer.id + negative_layers layer_data.addLayer(abs_layer_number) this_layer = layer_data.getLayer(abs_layer_number) layer_data.setLayerHeight(abs_layer_number, layer.height) layer_data.setLayerThickness(abs_layer_number, layer.thickness) for p in range(layer.repeatedMessageCount("path_segment")): polygon = layer.getRepeatedMessage("path_segment", p) extruder = polygon.extruder line_types = numpy.fromstring( polygon.line_type, dtype="u1") # Convert bytearray to numpy array line_types = line_types.reshape((-1, 1)) points = numpy.fromstring( polygon.points, dtype="f4") # Convert bytearray to numpy array if polygon.point_type == 0: # Point2D points = points.reshape( (-1, 2) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. else: # Point3D points = points.reshape((-1, 3)) line_widths = numpy.fromstring( polygon.line_width, dtype="f4") # Convert bytearray to numpy array line_widths = line_widths.reshape( (-1, 1) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. line_thicknesses = numpy.fromstring( polygon.line_thickness, dtype="f4") # Convert bytearray to numpy array line_thicknesses = line_thicknesses.reshape( (-1, 1) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. line_feedrates = numpy.fromstring( polygon.line_feedrate, dtype="f4") # Convert bytearray to numpy array line_feedrates = line_feedrates.reshape( (-1, 1) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. # Create a new 3D-array, copy the 2D points over and insert the right height. # This uses manual array creation + copy rather than numpy.insert since this is # faster. new_points = numpy.empty((len(points), 3), numpy.float32) if polygon.point_type == 0: # Point2D new_points[:, 0] = points[:, 0] new_points[:, 1] = layer.height / 1000 # layer height value is in backend representation new_points[:, 2] = -points[:, 1] else: # Point3D new_points[:, 0] = points[:, 0] new_points[:, 1] = points[:, 2] new_points[:, 2] = -points[:, 1] this_poly = LayerPolygon.LayerPolygon(extruder, line_types, new_points, line_widths, line_thicknesses, line_feedrates) this_poly.buildCache() this_layer.polygons.append(this_poly) Job.yieldThread() Job.yieldThread() current_layer += 1 progress = (current_layer / layer_count) * 99 # TODO: Rebuild the layer data mesh once the layer has been processed. # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh. if self._abort_requested: if self._progress_message: self._progress_message.hide() return if self._progress_message: self._progress_message.setProgress(progress) # We are done processing all the layers we got from the engine, now create a mesh out of the data # Find out colors per extruder global_container_stack = Application.getInstance( ).getGlobalContainerStack() manager = ExtruderManager.getInstance() extruders = list( manager.getMachineExtruders(global_container_stack.getId())) if extruders: material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32) for extruder in extruders: position = int( extruder.getMetaDataEntry("position", default="0")) # Get the position try: default_color = ExtrudersModel.defaultColors[position] except IndexError: default_color = "#e0e000" color_code = extruder.material.getMetaDataEntry( "color_code", default=default_color) color = colorCodeToRGBA(color_code) material_color_map[position, :] = color else: # Single extruder via global stack. material_color_map = numpy.zeros((1, 4), dtype=numpy.float32) color_code = global_container_stack.material.getMetaDataEntry( "color_code", default="#e0e000") color = colorCodeToRGBA(color_code) material_color_map[0, :] = color # We have to scale the colors for compatibility mode if OpenGLContext.isLegacyOpenGL() or bool( Application.getInstance().getPreferences().getValue( "view/force_layer_view_compatibility_mode")): line_type_brightness = 0.5 # for compatibility mode else: line_type_brightness = 1.0 layer_mesh = layer_data.build(material_color_map, line_type_brightness) if self._abort_requested: if self._progress_message: self._progress_message.hide() return # Add LayerDataDecorator to scene node to indicate that the node has layer data decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_mesh) new_node.addDecorator(decorator) new_node.setMeshData(mesh) # Set build volume as parent, the build volume can move as a result of raft settings. # It makes sense to set the build volume as parent: the print is actually printed on it. new_node_parent = Application.getInstance().getBuildVolume() new_node.setParent( new_node_parent) # Note: After this we can no longer abort! settings = Application.getInstance().getGlobalContainerStack() if not settings.getProperty("machine_center_is_zero", "value"): new_node.setPosition( Vector(-settings.getProperty("machine_width", "value") / 2, 0.0, settings.getProperty("machine_depth", "value") / 2)) if self._progress_message: self._progress_message.setProgress(100) if self._progress_message: self._progress_message.hide() # Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed. self._layers = None Logger.log("d", "Processing layers took %s seconds", time() - start_time)
def run(self): if Application.getInstance().getController().getActiveView( ).getPluginId() == "LayerView": self._progress = Message( catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._progress.show() Job.yieldThread() if self._abort_requested: if self._progress: self._progress.hide() return Application.getInstance().getController().activeViewChanged.connect( self._onActiveViewChanged) object_id_map = {} new_node = SceneNode() ## Remove old layer data (if any) for node in DepthFirstIterator(self._scene.getRoot()): if type(node) is SceneNode and node.getMeshData(): if node.callDecoration("getLayerData"): self._scene.getRoot().removeChild(node) Job.yieldThread() if self._abort_requested: if self._progress: self._progress.hide() return settings = Application.getInstance().getMachineManager( ).getWorkingProfile() mesh = MeshData() layer_data = LayerData.LayerData() layer_count = len(self._layers) current_layer = 0 for layer in self._layers: layer_data.addLayer(layer.id) layer_data.setLayerHeight(layer.id, layer.height) layer_data.setLayerThickness(layer.id, layer.thickness) for p in range(layer.repeatedMessageCount("polygons")): polygon = layer.getRepeatedMessage("polygons", p) points = numpy.fromstring( polygon.points, dtype="i8") # Convert bytearray to numpy array points = points.reshape( (-1, 2) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. # Create a new 3D-array, copy the 2D points over and insert the right height. # This uses manual array creation + copy rather than numpy.insert since this is # faster. new_points = numpy.empty((len(points), 3), numpy.float32) new_points[:, 0] = points[:, 0] new_points[:, 1] = layer.height new_points[:, 2] = -points[:, 1] new_points /= 1000 layer_data.addPolygon(layer.id, polygon.type, new_points, polygon.line_width) Job.yieldThread() Job.yieldThread() current_layer += 1 progress = (current_layer / layer_count) * 100 # TODO: Rebuild the layer data mesh once the layer has been processed. # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh. if self._abort_requested: if self._progress: self._progress.hide() return if self._progress: 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() if self._abort_requested: if self._progress: self._progress.hide() return #Add layerdata decorator to scene node to indicate that the node has layerdata decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_data) new_node.addDecorator(decorator) new_node.setMeshData(mesh) new_node.setParent( self._scene.getRoot()) #Note: After this we can no longer abort! if not settings.getSettingValue("machine_center_is_zero"): new_node.setPosition( Vector(-settings.getSettingValue("machine_width") / 2, 0.0, settings.getSettingValue("machine_depth") / 2)) if self._progress: self._progress.setProgress(100) view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "LayerView": view.resetLayerData() if self._progress: self._progress.hide()
def render(self): if not self._layer_shader: if self._compatibility_mode: shader_filename = "layers.shader" shadow_shader_filename = "layers_shadow.shader" else: shader_filename = "layers3d.shader" shadow_shader_filename = "layers3d_shadow.shader" self._layer_shader = OpenGL.getInstance().createShaderProgram( os.path.join( PluginRegistry.getInstance().getPluginPath( "SimulationView"), shader_filename)) self._layer_shadow_shader = OpenGL.getInstance( ).createShaderProgram( os.path.join( PluginRegistry.getInstance().getPluginPath( "SimulationView"), shadow_shader_filename)) self._current_shader = self._layer_shader self._updateLayerShaderValues() if not self._tool_handle_shader: self._tool_handle_shader = OpenGL.getInstance( ).createShaderProgram( Resources.getPath(Resources.Shaders, "toolhandle.shader")) if not self._nozzle_shader: self._nozzle_shader = OpenGL.getInstance().createShaderProgram( Resources.getPath(Resources.Shaders, "color.shader")) self._nozzle_shader.setUniformValue( "u_color", Color(*Application.getInstance().getTheme().getColor( "layerview_nozzle").getRgb())) self.bind() tool_handle_batch = RenderBatch(self._tool_handle_shader, type=RenderBatch.RenderType.Overlay, backface_cull=True) head_position = None # Indicates the current position of the print head nozzle_node = None for node in DepthFirstIterator(self._scene.getRoot()): if isinstance(node, ToolHandle): tool_handle_batch.addItem(node.getWorldTransformation(), mesh=node.getSolidMesh()) elif isinstance(node, NozzleNode): nozzle_node = node nozzle_node.setVisible(False) elif isinstance(node, SceneNode) and (node.getMeshData( ) or node.callDecoration("isBlockSlicing")) and node.isVisible(): layer_data = node.callDecoration("getLayerData") if not layer_data: continue # Render all layers below a certain number as line mesh instead of vertices. if self._layer_view._current_layer_num > -1 and ( (not self._layer_view._only_show_top_layers) or (not self._layer_view.getCompatibilityMode())): start = self._layer_view.start_elements_index end = self._layer_view.end_elements_index index = self._layer_view._current_path_num offset = 0 for polygon in layer_data.getLayer( self._layer_view._current_layer_num).polygons: # The size indicates all values in the two-dimension array, and the second dimension is # always size 3 because we have 3D points. if index >= polygon.data.size // 3 - offset: index -= polygon.data.size // 3 - offset offset = 1 # This is to avoid the first point when there is more than one polygon, since has the same value as the last point in the previous polygon continue # The head position is calculated and translated head_position = Vector( polygon.data[index + offset][0], polygon.data[index + offset][1], polygon.data[index + offset][2]) + node.getWorldPosition() break # Calculate the range of paths in the last layer current_layer_start = end current_layer_end = end + self._layer_view._current_path_num * 2 # Because each point is used twice # This uses glDrawRangeElements internally to only draw a certain range of lines. # All the layers but the current selected layer are rendered first if self._old_current_path != self._layer_view._current_path_num: self._current_shader = self._layer_shadow_shader self._switching_layers = False if not self._layer_view.isSimulationRunning( ) and self._old_current_layer != self._layer_view._current_layer_num: self._current_shader = self._layer_shader self._switching_layers = True layers_batch = RenderBatch( self._current_shader, type=RenderBatch.RenderType.Solid, mode=RenderBatch.RenderMode.Lines, range=(start, end), backface_cull=True) layers_batch.addItem(node.getWorldTransformation(), layer_data) layers_batch.render(self._scene.getActiveCamera()) # Current selected layer is rendered current_layer_batch = RenderBatch( self._layer_shader, type=RenderBatch.RenderType.Solid, mode=RenderBatch.RenderMode.Lines, range=(current_layer_start, current_layer_end)) current_layer_batch.addItem(node.getWorldTransformation(), layer_data) current_layer_batch.render(self._scene.getActiveCamera()) self._old_current_layer = self._layer_view._current_layer_num self._old_current_path = self._layer_view._current_path_num # Create a new batch that is not range-limited batch = RenderBatch(self._layer_shader, type=RenderBatch.RenderType.Solid) if self._layer_view.getCurrentLayerMesh(): batch.addItem(node.getWorldTransformation(), self._layer_view.getCurrentLayerMesh()) if self._layer_view.getCurrentLayerJumps(): batch.addItem(node.getWorldTransformation(), self._layer_view.getCurrentLayerJumps()) if len(batch.items) > 0: batch.render(self._scene.getActiveCamera()) # The nozzle is drawn when once we know the correct position of the head, # but the user is not using the layer slider, and the compatibility mode is not enabled if not self._switching_layers and not self._compatibility_mode and self._layer_view.getActivity( ) and nozzle_node is not None: if head_position is not None: nozzle_node.setVisible(True) nozzle_node.setPosition(head_position) nozzle_batch = RenderBatch( self._nozzle_shader, type=RenderBatch.RenderType.Transparent) nozzle_batch.addItem(nozzle_node.getWorldTransformation(), mesh=nozzle_node.getMeshData()) nozzle_batch.render(self._scene.getActiveCamera()) # Render toolhandles on top of the layerview if len(tool_handle_batch.items) > 0: tool_handle_batch.render(self._scene.getActiveCamera()) self.release()
def _onChangeTimerFinished(self): if not self._enabled: return root = self._controller.getScene().getRoot() # Keep a list of nodes that are moving. We use this so that we don't move two intersecting objects in the # same direction. transformed_nodes = [] for node in BreadthFirstIterator(root): if node is root or type( node) is not SceneNode or node.getBoundingBox() is None: continue bbox = node.getBoundingBox() # Ignore intersections with the bottom build_volume_bounding_box = self._build_volume.getBoundingBox() if build_volume_bounding_box: # It's over 9000! build_volume_bounding_box = build_volume_bounding_box.set( bottom=-9001) else: # No bounding box. This is triggered when running Cura from command line with a model for the first time # In that situation there is a model, but no machine (and therefore no build volume. return node._outside_buildarea = False # Mark the node as outside the build volume if the bounding box test fails. if build_volume_bounding_box.intersectsBox( bbox ) != AxisAlignedBox.IntersectionResult.FullIntersection: node._outside_buildarea = True # Move it downwards if bottom is above platform move_vector = Vector() if Preferences.getInstance().getValue( "physics/automatic_drop_down") and 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 move_vector = move_vector.set(y=-bbox.bottom + z_offset) # If there is no convex hull for the node, start calculating it and continue. if not node.getDecorator(ConvexHullDecorator): node.addDecorator(ConvexHullDecorator()) 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. if other_node is root or type( other_node) is not SceneNode or other_node is node: continue # Ignore collisions of a group with it's own children if other_node in node.getAllChildren( ) or node in other_node.getAllChildren(): continue # Ignore collisions within a group if other_node.getParent().callDecoration( "isGroup") is not None or node.getParent( ).callDecoration("isGroup") is not None: continue # Ignore nodes that do not have the right properties set. if not other_node.callDecoration( "getConvexHull") or not other_node.getBoundingBox( ): continue if other_node in transformed_nodes: continue # Other node is already moving, wait for next pass. # Get the overlap distance for both convex hulls. If this returns None, there is no intersection. head_hull = node.callDecoration("getConvexHullHead") if head_hull: overlap = head_hull.intersectsPolygon( other_node.callDecoration("getConvexHullHead")) if not overlap: other_head_hull = other_node.callDecoration( "getConvexHullHead") if other_head_hull: overlap = node.callDecoration( "getConvexHullHead").intersectsPolygon( other_head_hull) else: own_convex_hull = node.callDecoration("getConvexHull") other_convex_hull = other_node.callDecoration( "getConvexHull") if own_convex_hull and other_convex_hull: overlap = own_convex_hull.intersectsPolygon( other_convex_hull) else: # This can happen in some cases if the object is not yet done with being loaded. # Simply waiting for the next tick seems to resolve this correctly. overlap = None if overlap is None: continue 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(): return # Check for collisions between disallowed areas and the object for area in self._build_volume.getDisallowedAreas(): overlap = convex_hull.intersectsPolygon(area) if overlap is None: continue node._outside_buildarea = True if not Vector.Null.equals(move_vector, epsilon=1e-5): transformed_nodes.append(node) op = PlatformPhysicsOperation.PlatformPhysicsOperation( node, move_vector) op.push()
def read(self, file_name): Logger.log("d", "Preparing to load %s" % file_name) self._cancelled = False scene_node = SceneNode() # Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no # real data to calculate it from. scene_node.getBoundingBox = self._getNullBoundingBox gcode_list = [] self._is_layers_in_file = False Logger.log("d", "Opening file %s" % file_name) self._extruder_offsets = self._extruderOffsets( ) # dict with index the extruder number. can be empty last_z = 0 with open(file_name, "r") as file: file_lines = 0 current_line = 0 for line in file: file_lines += 1 gcode_list.append(line) if not self._is_layers_in_file and line[:len( self._layer_keyword)] == self._layer_keyword: self._is_layers_in_file = True file.seek(0) file_step = max(math.floor(file_lines / 100), 1) self._clearValues() self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"), lifetime=0) self._message.setProgress(0) self._message.show() Logger.log("d", "Parsing %s..." % file_name) current_position = self._position(0, 0, 0, [0]) current_path = [] for line in file: if self._cancelled: Logger.log("d", "Parsing %s cancelled" % file_name) return None current_line += 1 last_z = current_position.z if current_line % file_step == 0: self._message.setProgress( math.floor(current_line / file_lines * 100)) Job.yieldThread() if len(line) == 0: continue if line.find(self._type_keyword) == 0: type = line[len(self._type_keyword):].strip() if type == "WALL-INNER": self._layer_type = LayerPolygon.InsetXType elif type == "WALL-OUTER": self._layer_type = LayerPolygon.Inset0Type elif type == "SKIN": self._layer_type = LayerPolygon.SkinType elif type == "SKIRT": self._layer_type = LayerPolygon.SkirtType elif type == "SUPPORT": self._layer_type = LayerPolygon.SupportType elif type == "FILL": self._layer_type = LayerPolygon.InfillType else: Logger.log( "w", "Encountered a unknown type (%s) while parsing g-code.", type) if self._is_layers_in_file and line[:len( self._layer_keyword)] == self._layer_keyword: try: layer_number = int(line[len(self._layer_keyword):]) self._createPolygon( self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])) current_path.clear() self._layer_number = layer_number except: pass # This line is a comment. Ignore it (except for the layer_keyword) if line.startswith(";"): continue G = self._getInt(line, "G") if G is not None: current_position = self._processGCode( G, line, current_position, current_path) # < 2 is a heuristic for a movement only, that should not be counted as a layer if current_position.z > last_z and abs(current_position.z - last_z) < 2: if self._createPolygon( self._current_layer_thickness, current_path, self._extruder_offsets.get( self._extruder_number, [0, 0])): current_path.clear() if not self._is_layers_in_file: self._layer_number += 1 continue if line.startswith("T"): T = self._getInt(line, "T") if T is not None: self._createPolygon( self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])) current_path.clear() current_position = self._processTCode( T, line, current_position, current_path) # "Flush" leftovers if not self._is_layers_in_file and len(current_path) > 1: if self._createPolygon( self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])): self._layer_number += 1 current_path.clear() material_color_map = numpy.zeros((10, 4), dtype=numpy.float32) material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0] material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0] layer_mesh = self._layer_data_builder.build(material_color_map) decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_mesh) scene_node.addDecorator(decorator) gcode_list_decorator = GCodeListDecorator() gcode_list_decorator.setGCodeList(gcode_list) scene_node.addDecorator(gcode_list_decorator) Application.getInstance().getController().getScene( ).gcode_list = gcode_list Logger.log("d", "Finished parsing %s" % file_name) self._message.hide() if self._layer_number == 0: Logger.log("w", "File %s doesn't contain any valid layers" % file_name) settings = Application.getInstance().getGlobalContainerStack() machine_width = settings.getProperty("machine_width", "value") machine_depth = settings.getProperty("machine_depth", "value") if not self._center_is_zero: scene_node.setPosition( Vector(-machine_width / 2, 0, machine_depth / 2)) Logger.log("d", "Loaded %s" % file_name) if Preferences.getInstance().getValue("gcodereader/show_caution"): caution_message = Message(catalog.i18nc( "@info:generic", "Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate." ), lifetime=0) caution_message.show() # The "save/print" button's state is bound to the backend state. backend = Application.getInstance().getBackend() backend.backendStateChange.emit(Backend.BackendState.Disabled) return scene_node
def run(self): status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"), lifetime = 0, dismissable=False, progress = 0, title = i18n_catalog.i18nc("@info:title", "Finding Location")) status_message.show() global_container_stack = Application.getInstance().getGlobalContainerStack() machine_width = global_container_stack.getProperty("machine_width", "value") machine_depth = global_container_stack.getProperty("machine_depth", "value") arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = self._fixed_nodes, min_offset = self._min_offset) # Build set to exclude children (those get arranged together with the parents). included_as_child = set() for node in self._nodes: included_as_child.update(node.getAllChildren()) # Collect nodes to be placed nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr) for node in self._nodes: if node in included_as_child: continue offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset, include_children = True) if offset_shape_arr is None: Logger.log("w", "Node [%s] could not be converted to an array for arranging...", str(node)) continue nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr)) # Sort the nodes with the biggest area first. nodes_arr.sort(key=lambda item: item[0]) nodes_arr.reverse() # Place nodes one at a time start_priority = 0 last_priority = start_priority last_size = None grouped_operation = GroupedOperation() found_solution_for_all = True not_fit_count = 0 for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr): # For performance reasons, we assume that when a location does not fit, # it will also not fit for the next object (while what can be untrue). if last_size == size: # This optimization works if many of the objects have the same size start_priority = last_priority else: start_priority = 0 best_spot = arranger.bestSpot(hull_shape_arr, start_prio = start_priority) x, y = best_spot.x, best_spot.y node.removeDecorator(ZOffsetDecorator) if node.getBoundingBox(): center_y = node.getWorldPosition().y - node.getBoundingBox().bottom else: center_y = 0 if x is not None: # We could find a place last_size = size last_priority = best_spot.priority arranger.place(x, y, offset_shape_arr) # take place before the next one grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True)) else: Logger.log("d", "Arrange all: could not find spot!") found_solution_for_all = False grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, -not_fit_count * 20), set_position = True)) not_fit_count += 1 status_message.setProgress((idx + 1) / len(nodes_arr) * 100) Job.yieldThread() grouped_operation.push() status_message.hide() if not found_solution_for_all: no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), title = i18n_catalog.i18nc("@info:title", "Can't Find Location")) no_full_solution_message.show() self.finished.emit(self)
class Selection: @classmethod def add(cls, object: SceneNode) -> None: if object not in cls.__selection: cls.__selection.append(object) object.transformationChanged.connect(cls._onTransformationChanged) cls._onTransformationChanged(object) cls.selectionChanged.emit() @classmethod def remove(cls, object: SceneNode) -> None: if object in cls.__selection: cls.__selection.remove(object) object.transformationChanged.disconnect( cls._onTransformationChanged) cls._onTransformationChanged(object) cls.selectionChanged.emit() @classmethod ## Get number of selected objects def getCount(cls) -> int: return len(cls.__selection) @classmethod def getAllSelectedObjects(cls) -> List[SceneNode]: return cls.__selection @classmethod def getBoundingBox(cls) -> AxisAlignedBox: bounding_box = None # don't start with an empty bounding box, because that includes (0,0,0) for node in cls.__selection: if not bounding_box: bounding_box = node.getBoundingBox() else: bounding_box = bounding_box + node.getBoundingBox() if not bounding_box: bounding_box = AxisAlignedBox.Null return bounding_box @classmethod ## Get selected object by index # \param index index of the object to return # \returns selected object or None if index was incorrect / not found def getSelectedObject(cls, index: int) -> Optional[SceneNode]: try: return cls.__selection[index] except IndexError: return None @classmethod def isSelected(cls, object: SceneNode) -> bool: return object in cls.__selection @classmethod def clear(cls): cls.__selection.clear() cls.selectionChanged.emit() @classmethod ## Check if anything is selected at all. def hasSelection(cls) -> bool: return bool(cls.__selection) selectionChanged = Signal() selectionCenterChanged = Signal() @classmethod def getSelectionCenter(cls) -> Vector: return cls.__selection_center ## Apply an operation to the entire selection # # This will create and push an operation onto the operation stack. Dependent # on whether there is one item selected or multiple it will be just the # operation or a grouped operation containing the operation for each selected # node. # # \param operation \type{Class} The operation to create and push. It should take a SceneNode as first positional parameter. # \param args The additional positional arguments passed along to the operation constructor. # \param kwargs The additional keyword arguments that will be passed along to the operation constructor. # # \return list of instantiated operations @classmethod def applyOperation(cls, operation, *args, **kwargs): if not cls.__selection: return operations = [] if len(cls.__selection) == 1: node = cls.__selection[0] op = operation(node, *args, **kwargs) operations.append(op) else: op = GroupedOperation() for node in Selection.getAllSelectedObjects(): sub_op = operation(node, *args, **kwargs) op.addOperation(sub_op) operations.append(sub_op) op.push() return operations @classmethod def _onTransformationChanged(cls, node): cls.__selection_center = cls.getBoundingBox().center cls.selectionCenterChanged.emit() __selection = [] # type: List[SceneNode] __selection_center = Vector(0, 0, 0)
def resetScale(self): """Reset scale of the selected objects""" Selection.applyOperation(SetTransformOperation, None, None, Vector(1.0, 1.0, 1.0), Vector(0, 0, 0))
def getTranslation(self) -> Vector: return Vector(data=self._data[:3, 3])
def event(self, event): """Handle mouse and keyboard events :param event: type(Event) """ super().event(event) if event.type == Event.ToolActivateEvent: for node in self._getSelectedObjectsWithoutSelectedAncestors(): node.boundingBoxChanged.connect(self.propertyChanged) if event.type == Event.ToolDeactivateEvent: for node in self._getSelectedObjectsWithoutSelectedAncestors(): node.boundingBoxChanged.disconnect(self.propertyChanged) # Handle modifier keys: Shift toggles snap, Control toggles uniform scaling if event.type == Event.KeyPressEvent: if event.key == KeyEvent.ShiftKey: self.setScaleSnap(not self._snap_scale) elif event.key == KeyEvent.ControlKey: self.setNonUniformScale(not self._non_uniform_scale) if event.type == Event.KeyReleaseEvent: if event.key == KeyEvent.ShiftKey: self.setScaleSnap(not self._snap_scale) elif event.key == KeyEvent.ControlKey: self.setNonUniformScale(not self._non_uniform_scale) if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(): # Initialise a scale operation if MouseEvent.LeftButton not in event.buttons: return False id = self._selection_pass.getIdAtPosition(event.x, event.y) if not id: return False if self._handle.isAxis(id): self.setLockedAxis(id) self._saved_handle_position = self._handle.getWorldPosition() # Save the current positions of the node, as we want to scale arround their current centres self._saved_node_positions = [] for node in self._getSelectedObjectsWithoutSelectedAncestors(): self._saved_node_positions.append((node, node.getPosition())) self._scale_sum = 0.0 self._last_event = event if id == ToolHandle.XAxis: self.setDragPlane(Plane(Vector(0, 0, 1), self._saved_handle_position.z)) elif id == ToolHandle.YAxis: self.setDragPlane(Plane(Vector(0, 0, 1), self._saved_handle_position.z)) elif id == ToolHandle.ZAxis: self.setDragPlane(Plane(Vector(0, 1, 0), self._saved_handle_position.y)) else: self.setDragPlane(Plane(Vector(0, 1, 0), self._saved_handle_position.y)) self.setDragStart(event.x, event.y) return True if event.type == Event.MouseMoveEvent: # Perform a scale operation if not self.getDragPlane(): return False drag_position = self.getDragPosition(event.x, event.y) if drag_position: if self.getLockedAxis() == ToolHandle.XAxis: drag_position = drag_position.set(y = 0, z = 0) elif self.getLockedAxis() == ToolHandle.YAxis: drag_position = drag_position.set(x = 0, z = 0) elif self.getLockedAxis() == ToolHandle.ZAxis: drag_position = drag_position.set(x = 0, y = 0) drag_length = (drag_position - self._saved_handle_position).length() if self._drag_length > 0: drag_change = (drag_length - self._drag_length) / 100 * self._scale_speed if self.getLockedAxis() in [ToolHandle.XAxis, ToolHandle.YAxis, ToolHandle.ZAxis]: # drag the handle, axis is already determined if self._snap_scale: scale_factor = round(drag_change, 1) else: scale_factor = drag_change else: # uniform scaling; because we use central cube, we use the screen x, y for scaling. # upper right is scale up, lower left is scale down scale_factor_delta = ((self._last_event.y - event.y) - (self._last_event.x - event.x)) * self._scale_speed self._scale_sum += scale_factor_delta if self._snap_scale: scale_factor = round(self._scale_sum, 1) # remember the decimals when snap scaling self._scale_sum -= scale_factor else: scale_factor = self._scale_sum self._scale_sum = 0.0 if scale_factor: scale_change = Vector(0.0, 0.0, 0.0) if self._non_uniform_scale: if self.getLockedAxis() == ToolHandle.XAxis: scale_change = scale_change.set(x=scale_factor) elif self.getLockedAxis() == ToolHandle.YAxis: scale_change = scale_change.set(y=scale_factor) elif self.getLockedAxis() == ToolHandle.ZAxis: scale_change = scale_change.set(z=scale_factor) else: # Middle handle scale_change = scale_change.set(x=scale_factor, y=scale_factor, z=scale_factor) else: scale_change = scale_change.set(x=scale_factor, y=scale_factor, z=scale_factor) # Scale around the saved centers of all selected nodes if len(self._saved_node_positions) > 1: op = GroupedOperation() for node, position in self._saved_node_positions: op.addOperation(ScaleOperation(node, scale_change, relative_scale = True, scale_around_point = position)) op.push() else: for node, position in self._saved_node_positions: ScaleOperation(node, scale_change, relative_scale = True, scale_around_point = position).push() self._drag_length = (self._saved_handle_position - drag_position).length() else: self.operationStarted.emit(self) self._drag_length = (self._saved_handle_position - drag_position).length() #First move, do nothing but set right length. self._last_event = event # remember for uniform drag return True if event.type == Event.MouseReleaseEvent: # Finish a scale operation if self.getDragPlane(): self.setDragPlane(None) self.setLockedAxis(ToolHandle.NoAxis) self._drag_length = 0 self.operationStopped.emit(self) return True
def decompose(self) -> Tuple[Vector, "Matrix", Vector, Vector]: """ SOURCE: https://github.com/matthew-brett/transforms3d/blob/e402e56686648d9a88aa048068333b41daa69d1a/transforms3d/affines.py Decompose 4x4 homogenous affine matrix into parts. The parts are translations, rotations, zooms, shears. This is the same as :func:`decompose` but specialized for 4x4 affines. Decomposes `A44` into ``T, R, Z, S``, such that:: Smat = np.array([[1, S[0], S[1]], [0, 1, S[2]], [0, 0, 1]]) RZS = np.dot(R, np.dot(np.diag(Z), Smat)) A44 = np.eye(4) A44[:3,:3] = RZS A44[:-1,-1] = T The order of transformations is therefore shears, followed by zooms, followed by rotations, followed by translations. This routine only works for shape (4,4) matrices Parameters ---------- A44 : array shape (4,4) Returns ------- T : array, shape (3,) Translation vector R : array shape (3,3) rotation matrix Z : array, shape (3,) Zoom vector. May have one negative zoom to prevent need for negative determinant R matrix above S : array, shape (3,) Shear vector, such that shears fill upper triangle above diagonal to form shear matrix (type ``striu``). """ A44 = numpy.asarray(self._data, dtype=numpy.float64) T = A44[:-1, -1] RZS = A44[:-1, :-1] # compute scales and shears M0, M1, M2 = numpy.array(RZS).T # extract x scale and normalize sx = math.sqrt(numpy.sum(M0**2)) M0 /= sx # orthogonalize M1 with respect to M0 sx_sxy = numpy.dot(M0, M1) M1 -= sx_sxy * M0 # extract y scale and normalize sy = math.sqrt(numpy.sum(M1**2)) M1 /= sy sxy = sx_sxy / sx # orthogonalize M2 with respect to M0 and M1 sx_sxz = numpy.dot(M0, M2) sy_syz = numpy.dot(M1, M2) M2 -= (sx_sxz * M0 + sy_syz * M1) # extract z scale and normalize sz = math.sqrt(numpy.sum(M2**2)) M2 /= sz sxz = sx_sxz / sx syz = sy_syz / sy # Reconstruct rotation matrix, ensure positive determinant Rmat = numpy.array([M0, M1, M2]).T # The original code ensures that the determinant is positive, but I can't find a single situation where this # is actualy used / needed by us. It is, however, one of the more expensive parts of this function. #if numpy.linalg.det(Rmat) < 0: # sx *= -1 # Rmat[:, 0] *= -1 return Vector(data=T), Matrix(data=Rmat), Vector( data=numpy.array([sx, sy, sz])), Vector( data=numpy.array([sxy, sxz, syz]))
def _read(self, file_name: str) -> Union[SceneNode, List[SceneNode]]: result = [] # The base object of 3mf is a zipped archive. try: archive = zipfile.ZipFile(file_name, "r") self._base_name = os.path.basename(file_name) parser = Savitar.ThreeMFParser() scene_3mf = parser.parse(archive.open("3D/3dmodel.model").read()) self._unit = scene_3mf.getUnit() for node in scene_3mf.getSceneNodes(): um_node = self._convertSavitarNodeToUMNode(node, file_name) if um_node is None: continue # compensate for original center position, if object(s) is/are not around its zero position transform_matrix = Matrix() mesh_data = um_node.getMeshData() if mesh_data is not None: extents = mesh_data.getExtents() if extents is not None: center_vector = Vector(extents.center.x, extents.center.y, extents.center.z) transform_matrix.setByTranslation(center_vector) transform_matrix.multiply(um_node.getLocalTransformation()) um_node.setTransformation(transform_matrix) global_container_stack = CuraApplication.getInstance( ).getGlobalContainerStack() # Create a transformation Matrix to convert from 3mf worldspace into ours. # First step: flip the y and z axis. transformation_matrix = Matrix() transformation_matrix._data[1, 1] = 0 transformation_matrix._data[1, 2] = 1 transformation_matrix._data[2, 1] = -1 transformation_matrix._data[2, 2] = 0 # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the # build volume. if global_container_stack: translation_vector = Vector( x=-global_container_stack.getProperty( "machine_width", "value") / 2, y=-global_container_stack.getProperty( "machine_depth", "value") / 2, z=0) translation_matrix = Matrix() translation_matrix.setByTranslation(translation_vector) transformation_matrix.multiply(translation_matrix) # Third step: 3MF also defines a unit, whereas Cura always assumes mm. scale_matrix = Matrix() scale_matrix.setByScaleVector( self._getScaleFromUnit(self._unit)) transformation_matrix.multiply(scale_matrix) # Pre multiply the transformation with the loaded transformation, so the data is handled correctly. um_node.setTransformation( um_node.getLocalTransformation().preMultiply( transformation_matrix)) # Check if the model is positioned below the build plate and honor that when loading project files. node_meshdata = um_node.getMeshData() if node_meshdata is not None: aabb = node_meshdata.getExtents( um_node.getWorldTransformation()) if aabb is not None: minimum_z_value = aabb.minimum.y # y is z in transformation coordinates if minimum_z_value < 0: um_node.addDecorator(ZOffsetDecorator()) um_node.callDecoration("setZOffset", minimum_z_value) result.append(um_node) except Exception: Logger.logException("e", "An exception occurred in 3mf reader.") return [] return result
def _onChangeTimerFinished(self): if not self._enabled: return root = self._controller.getScene().getRoot() # Keep a list of nodes that are moving. We use this so that we don't move two intersecting objects in the # same direction. transformed_nodes = [] group_nodes = [] # We try to shuffle all the nodes to prevent "locked" situations, where iteration B inverts iteration A. # By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve. nodes = list(BreadthFirstIterator(root)) random.shuffle(nodes) for node in nodes: if node is root or type(node) is not SceneNode or node.getBoundingBox() is None: continue bbox = node.getBoundingBox() # Ignore intersections with the bottom build_volume_bounding_box = self._build_volume.getBoundingBox() if build_volume_bounding_box: # It's over 9000! build_volume_bounding_box = build_volume_bounding_box.set(bottom=-9001) else: # No bounding box. This is triggered when running Cura from command line with a model for the first time # In that situation there is a model, but no machine (and therefore no build volume. return node._outside_buildarea = False # Mark the node as outside the build volume if the bounding box test fails. if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection: node._outside_buildarea = True if node.callDecoration("isGroup"): group_nodes.append(node) # Keep list of affected group_nodes # Move it downwards if bottom is above platform move_vector = Vector() if Preferences.getInstance().getValue("physics/automatic_drop_down") and 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 move_vector = move_vector.set(y=-bbox.bottom + z_offset) # If there is no convex hull for the node, start calculating it and continue. if not node.getDecorator(ConvexHullDecorator): node.addDecorator(ConvexHullDecorator()) 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. if other_node is root or type(other_node) is not SceneNode or other_node is node: continue # Ignore collisions of a group with it's own children if other_node in node.getAllChildren() or node in other_node.getAllChildren(): continue # Ignore collisions within a group if other_node.getParent().callDecoration("isGroup") is not None or node.getParent().callDecoration("isGroup") is not None: continue # Ignore nodes that do not have the right properties set. if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox(): continue if other_node in transformed_nodes: continue # Other node is already moving, wait for next pass. overlap = (0, 0) # Start loop with no overlap current_overlap_checks = 0 # Continue to check the overlap until we no longer find one. while overlap and current_overlap_checks < self._max_overlap_checks: current_overlap_checks += 1 head_hull = node.callDecoration("getConvexHullHead") if head_hull: # One at a time intersection. overlap = head_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_node.callDecoration("getConvexHull")) if not overlap: other_head_hull = other_node.callDecoration("getConvexHullHead") if other_head_hull: overlap = node.callDecoration("getConvexHull").translate(move_vector.x, move_vector.z).intersectsPolygon(other_head_hull) if overlap: # Moving ensured that overlap was still there. Try anew! move_vector = move_vector.set(x=move_vector.x + overlap[0] * self._move_factor, z=move_vector.z + overlap[1] * self._move_factor) else: # Moving ensured that overlap was still there. Try anew! move_vector = move_vector.set(x=move_vector.x + overlap[0] * self._move_factor, z=move_vector.z + overlap[1] * self._move_factor) else: own_convex_hull = node.callDecoration("getConvexHull") other_convex_hull = other_node.callDecoration("getConvexHull") if own_convex_hull and other_convex_hull: overlap = own_convex_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_convex_hull) if overlap: # Moving ensured that overlap was still there. Try anew! move_vector = move_vector.set(x=move_vector.x + overlap[0] * self._move_factor, z=move_vector.z + overlap[1] * self._move_factor) else: # This can happen in some cases if the object is not yet done with being loaded. # Simply waiting for the next tick seems to resolve this correctly. overlap = None convex_hull = node.callDecoration("getConvexHull") if convex_hull: if not convex_hull.isValid(): return # Check for collisions between disallowed areas and the object for area in self._build_volume.getDisallowedAreas(): overlap = convex_hull.intersectsPolygon(area) if overlap is None: continue node._outside_buildarea = True if not Vector.Null.equals(move_vector, epsilon=1e-5): transformed_nodes.append(node) op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector) op.push() # Group nodes should override the _outside_buildarea property of their children. for group_node in group_nodes: for child_node in group_node.getAllChildren(): child_node._outside_buildarea = group_node._outside_buildarea
def loadStep(self, step): selected_node = Selection.getSelectedObject(0) for bc in step.boundary_conditions: selected_face = self._interactive_mesh.face_from_ids(bc.face) face = AnchorFace(str(bc.name)) face.selection = (selected_node, bc.face[0]) if len(selected_face.triangles) > 0: face.surface_type = self._guessSurfaceTypeFromTriangles( selected_face) axis = None if face.surface_type == HighlightFace.SurfaceType.Flat: axis = selected_face.planar_axis() elif face.surface_type != face.SurfaceType.Unknown: axis = selected_face.rotation_axis() face.setMeshDataFromPywimTriangles(selected_face, axis) face.disableTools() self.addFace(face) for bc in step.loads: selected_face = self._interactive_mesh.face_from_ids(bc.face) face = LoadFace(str(bc.name)) face.selection = (selected_node, bc.face[0]) load_prime = Vector(bc.force[0], bc.force[1], bc.force[2]) origin_prime = Vector(bc.origin[0], bc.origin[1], bc.origin[2]) print_to_cura = Matrix() print_to_cura._data[1, 1] = 0 print_to_cura._data[1, 2] = 1 print_to_cura._data[2, 1] = -1 print_to_cura._data[2, 2] = 0 _, rotation, _, _ = print_to_cura.decompose() load = numpy.dot(rotation.getData(), load_prime.getData()) origin = numpy.dot(rotation.getData(), origin_prime.getData()) rotated_load = pywim.geom.Vector(load[0], load[1], load[2]) rotated_load.origin = pywim.geom.Vertex(origin[0], origin[1], origin[2]) if len(selected_face.triangles) > 0: face.surface_type = self._guessSurfaceTypeFromTriangles( selected_face) axis = None if face.surface_type == HighlightFace.SurfaceType.Flat: axis = selected_face.planar_axis() elif face.surface_type != face.SurfaceType.Unknown: axis = selected_face.rotation_axis() face.force.setFromVectorAndAxis(rotated_load, axis) # Need to reverse the load direction for concave / convex surface if face.surface_type == HighlightFace.SurfaceType.Concave or face.surface_type == HighlightFace.SurfaceType.Convex: if face.force.direction_type == Force.DirectionType.Normal: face.force.direction_type = Force.DirectionType.Parallel else: face.force.direction_type = Force.DirectionType.Normal face.setMeshDataFromPywimTriangles(selected_face, axis) face.setArrow(Vector(load[0], load[1], load[2])) self.addFace(face) face.disableTools()