def _read(self, file_name: str) -> Union["SceneNode", List["SceneNode"]]: """Reads a file using Trimesh. :param file_name: The file path. This is assumed to be one of the file types that Trimesh can read. It will not be checked again. :return: A scene node that contains the file's contents. """ # CURA-6739 # GLTF files are essentially JSON files. If you directly give a file name to trimesh.load(), it will # try to figure out the format, but for GLTF, it loads it as a binary file with flags "rb", and the json.load() # doesn't like it. For some reason, this seems to happen with 3.5.7, but not 3.7.1. Below is a workaround to # pass a file object that has been opened with "r" instead "rb" to load a GLTF file. if file_name.lower().endswith(".gltf"): mesh_or_scene = trimesh.load(open(file_name, "r", encoding="utf-8"), file_type="gltf") else: mesh_or_scene = trimesh.load(file_name) meshes = [] # type: List[Union[trimesh.Trimesh, trimesh.Scene, Any]] if isinstance(mesh_or_scene, trimesh.Trimesh): meshes = [mesh_or_scene] elif isinstance(mesh_or_scene, trimesh.Scene): meshes = [mesh for mesh in mesh_or_scene.geometry.values()] active_build_plate = CuraApplication.getInstance( ).getMultiBuildPlateModel().activeBuildPlate nodes = [] # type: List[SceneNode] for mesh in meshes: if not isinstance( mesh, trimesh.Trimesh ): # Trimesh can also receive point clouds, 2D paths, 3D paths or metadata. Skip those. continue mesh.merge_vertices() mesh.remove_unreferenced_vertices() mesh.fix_normals() mesh_data = self._toMeshData(mesh, file_name) file_base_name = os.path.basename(file_name) new_node = CuraSceneNode() new_node.setMeshData(mesh_data) new_node.setSelectable(True) new_node.setName(file_base_name if len( meshes) == 1 else "{file_base_name} {counter}".format( file_base_name=file_base_name, counter=str(len(nodes) + 1))) new_node.addDecorator(BuildPlateDecorator(active_build_plate)) new_node.addDecorator(SliceableObjectDecorator()) nodes.append(new_node) if len(nodes) == 1: return nodes[0] # Add all nodes to a group so they stay together. group_node = CuraSceneNode() group_node.addDecorator(GroupDecorator()) group_node.addDecorator(ConvexHullDecorator()) group_node.addDecorator(BuildPlateDecorator(active_build_plate)) for node in nodes: node.setParent(group_node) return group_node
def convex_hull_decorator(): with patch("cura.CuraApplication.CuraApplication.getInstance", MagicMock(return_value=mocked_application)): with patch("UM.Application.Application.getInstance", MagicMock(return_value=mocked_application)): with patch( "cura.Settings.ExtruderManager.ExtruderManager.getInstance" ): return ConvexHullDecorator()
def _read(self, file_name: str) -> Union["SceneNode", List["SceneNode"]]: mesh_or_scene = trimesh.load(file_name) meshes = [] # type: List[Union[trimesh.Trimesh, trimesh.Scene, Any]] if isinstance(mesh_or_scene, trimesh.Trimesh): meshes = [mesh_or_scene] elif isinstance(mesh_or_scene, trimesh.Scene): meshes = [mesh for mesh in mesh_or_scene.geometry.values()] active_build_plate = CuraApplication.getInstance( ).getMultiBuildPlateModel().activeBuildPlate nodes = [] # type: List[SceneNode] for mesh in meshes: if not isinstance( mesh, trimesh.Trimesh ): # Trimesh can also receive point clouds, 2D paths, 3D paths or metadata. Skip those. continue mesh.merge_vertices() mesh.remove_unreferenced_vertices() mesh.fix_normals() mesh_data = self._toMeshData(mesh) file_base_name = os.path.basename(file_name) new_node = CuraSceneNode() new_node.setMeshData(mesh_data) new_node.setSelectable(True) new_node.setName(file_base_name if len( meshes) == 1 else "{file_base_name} {counter}".format( file_base_name=file_base_name, counter=str(len(nodes) + 1))) new_node.addDecorator(BuildPlateDecorator(active_build_plate)) new_node.addDecorator(SliceableObjectDecorator()) nodes.append(new_node) if len(nodes) == 1: return nodes[0] # Add all nodes to a group so they stay together. group_node = CuraSceneNode() group_node.addDecorator(GroupDecorator()) group_node.addDecorator(ConvexHullDecorator()) group_node.addDecorator(BuildPlateDecorator(active_build_plate)) for node in nodes: node.setParent(group_node) return group_node
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 = [] # 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 not isinstance(node, SceneNode) or isinstance( node, DuplicatedNode) 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()) # only push away objects if this node is a printing mesh if not node.callDecoration( "isNonPrintingMesh") and Preferences.getInstance( ).getValue("physics/automatic_push_free" ) and type(node) != DuplicatedNode: # 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 not issubclass( type(other_node), SceneNode ) or other_node is node or other_node.callDecoration( "getBuildPlateNumber") != node.callDecoration( "getBuildPlateNumber"): 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. if other_node.callDecoration("isNonPrintingMesh"): continue 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! temp_move_vector = move_vector.set( x=move_vector.x + overlap[0] * self._move_factor, z=move_vector.z + overlap[1] * self._move_factor) # if the distance between two models less than 2mm then try to find a new factor if abs(temp_move_vector.x - overlap[0] ) < self._minimum_gap and abs( temp_move_vector.y - overlap[1]) < self._minimum_gap: temp_x_factor = ( abs(overlap[0]) + self._minimum_gap ) / overlap[0] if overlap[ 0] != 0 else 0 # find x move_factor, like (3.4 + 2) / 3.4 = 1.58 temp_y_factor = ( abs(overlap[1]) + self._minimum_gap ) / overlap[1] if overlap[ 1] != 0 else 0 # find y move_factor temp_scale_factor = temp_x_factor if abs( temp_x_factor) > abs( temp_y_factor ) else temp_y_factor move_vector = move_vector.set( x=move_vector.x + overlap[0] * temp_scale_factor, z=move_vector.z + overlap[1] * temp_scale_factor) else: move_vector = temp_move_vector 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 _convertSavitarNodeToUMNode( self, savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]: """Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node. :returns: Scene node. """ try: node_name = savitar_node.getName() node_id = savitar_node.getId() except AttributeError: Logger.log( "e", "Outdated version of libSavitar detected! Please update to the newest version!" ) node_name = "" node_id = "" if node_name == "": if file_name != "": node_name = os.path.basename(file_name) else: node_name = "Object {}".format(node_id) active_build_plate = CuraApplication.getInstance( ).getMultiBuildPlateModel().activeBuildPlate um_node = CuraSceneNode() # This adds a SettingOverrideDecorator um_node.addDecorator(BuildPlateDecorator(active_build_plate)) try: um_node.addDecorator(ConvexHullDecorator()) except: pass um_node.setName(node_name) um_node.setId(node_id) transformation = self._createMatrixFromTransformationString( savitar_node.getTransformation()) um_node.setTransformation(transformation) mesh_builder = MeshBuilder() data = numpy.fromstring( savitar_node.getMeshData().getFlatVerticesAsBytes(), dtype=numpy.float32) vertices = numpy.resize(data, (int(data.size / 3), 3)) mesh_builder.setVertices(vertices) mesh_builder.calculateNormals(fast=True) if file_name: # The filename is used to give the user the option to reload the file if it is changed on disk # It is only set for the root node of the 3mf file mesh_builder.setFileName(file_name) mesh_data = mesh_builder.build() if len(mesh_data.getVertices()): um_node.setMeshData(mesh_data) for child in savitar_node.getChildren(): child_node = self._convertSavitarNodeToUMNode(child) if child_node: um_node.addChild(child_node) if um_node.getMeshData() is None and len(um_node.getChildren()) == 0: return None settings = savitar_node.getSettings() # Add the setting override decorator, so we can add settings to this node. if settings: global_container_stack = CuraApplication.getInstance( ).getGlobalContainerStack() # Ensure the correct next container for the SettingOverride decorator is set. if global_container_stack: default_stack = ExtruderManager.getInstance().getExtruderStack( 0) if default_stack: um_node.callDecoration("setActiveExtruder", default_stack.getId()) # Get the definition & set it definition_id = ContainerTree.getInstance().machines[ global_container_stack.definition.getId( )].quality_definition um_node.callDecoration("getStack").getTop().setDefinition( definition_id) setting_container = um_node.callDecoration("getStack").getTop() known_setting_keys = um_node.callDecoration( "getStack").getAllKeys() for key in settings: setting_value = settings[key].value # Extruder_nr is a special case. if key == "extruder_nr": extruder_stack = ExtruderManager.getInstance( ).getExtruderStack(int(setting_value)) if extruder_stack: um_node.callDecoration("setActiveExtruder", extruder_stack.getId()) else: Logger.log("w", "Unable to find extruder in position %s", setting_value) continue if key in known_setting_keys: setting_container.setProperty(key, "value", setting_value) else: um_node.metadata[key] = settings[key] if len(um_node.getChildren()) > 0 and um_node.getMeshData() is None: if len(um_node.getAllChildren()) == 1: # We don't want groups of one, so move the node up one "level" child_node = um_node.getChildren()[0] parent_transformation = um_node.getLocalTransformation() child_transformation = child_node.getLocalTransformation() child_node.setTransformation( parent_transformation.multiply(child_transformation)) um_node = cast(CuraSceneNode, um_node.getChildren()[0]) else: group_decorator = GroupDecorator() um_node.addDecorator(group_decorator) um_node.setSelectable(True) if um_node.getMeshData(): # Assuming that all nodes with mesh data are printable objects # affects (auto) slicing sliceable_decorator = SliceableObjectDecorator() um_node.addDecorator(sliceable_decorator) return um_node
def _read(self, file_name): base_name = os.path.basename(file_name) try: zipped_file = zipfile.ZipFile(file_name) xml_document = zipped_file.read(zipped_file.namelist()[0]) zipped_file.close() except zipfile.BadZipfile: raw_file = open(file_name, "r") xml_document = raw_file.read() raw_file.close() try: amf_document = ET.fromstring(xml_document) except ET.ParseError: Logger.log("e", "Could not parse XML in file %s" % base_name) return None if "unit" in amf_document.attrib: unit = amf_document.attrib["unit"].lower() else: unit = "millimeter" if unit == "millimeter": scale = 1.0 elif unit == "meter": scale = 1000.0 elif unit == "inch": scale = 25.4 elif unit == "feet": scale = 304.8 elif unit == "micron": scale = 0.001 else: Logger.log("w", "Unknown unit in amf: %s. Using mm instead." % unit) scale = 1.0 nodes = [] for amf_object in amf_document.iter("object"): for amf_mesh in amf_object.iter("mesh"): amf_mesh_vertices = [] for vertices in amf_mesh.iter("vertices"): for vertex in vertices.iter("vertex"): for coordinates in vertex.iter("coordinates"): v = [0.0, 0.0, 0.0] for t in coordinates: if t.tag == "x": v[0] = float(t.text) * scale elif t.tag == "y": v[2] = -float(t.text) * scale elif t.tag == "z": v[1] = float(t.text) * scale amf_mesh_vertices.append(v) if not amf_mesh_vertices: continue indices = [] for volume in amf_mesh.iter("volume"): for triangle in volume.iter("triangle"): f = [0, 0, 0] for t in triangle: if t.tag == "v1": f[0] = int(t.text) elif t.tag == "v2": f[1] = int(t.text) elif t.tag == "v3": f[2] = int(t.text) indices.append(f) mesh = trimesh.base.Trimesh( vertices=numpy.array(amf_mesh_vertices, dtype=numpy.float32), faces=numpy.array(indices, dtype=numpy.int32)) mesh.merge_vertices() mesh.remove_unreferenced_vertices() mesh.fix_normals() mesh_data = self._toMeshData(mesh, file_name) new_node = CuraSceneNode() new_node.setSelectable(True) new_node.setMeshData(mesh_data) new_node.setName(base_name if len(nodes) == 0 else "%s %d" % (base_name, len(nodes))) new_node.addDecorator( BuildPlateDecorator(CuraApplication.getInstance( ).getMultiBuildPlateModel().activeBuildPlate)) new_node.addDecorator(SliceableObjectDecorator()) nodes.append(new_node) if not nodes: Logger.log("e", "No meshes in file %s" % base_name) return None if len(nodes) == 1: return nodes[0] # Add all scenenodes to a group so they stay together group_node = CuraSceneNode() group_node.addDecorator(GroupDecorator()) group_node.addDecorator(ConvexHullDecorator()) group_node.addDecorator( BuildPlateDecorator(CuraApplication.getInstance(). getMultiBuildPlateModel().activeBuildPlate)) for node in nodes: node.setParent(group_node) return group_node
def _readMeshFinished(self, job): Logger.log("d", "read mesh finisihed!") ### START PATCH: detect belt printer global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: return is_belt_printer = self._preferences.getValue("BeltPlugin/on_plugin") ### END PATCH nodes = job.getResult() file_name = job.getFileName() file_name_lower = file_name.lower() file_extension = file_name_lower.split(".")[-1] self._application._currently_loading_files.remove(file_name) self._application.fileLoaded.emit(file_name) target_build_plate = self._application.getMultiBuildPlateModel( ).activeBuildPlate root = self._application.getController().getScene().getRoot() fixed_nodes = [] for node_ in DepthFirstIterator(root): if node_.callDecoration("isSliceable") and node_.callDecoration( "getBuildPlateNumber") == target_build_plate: fixed_nodes.append(node_) global_container_stack = self._application.getGlobalContainerStack() machine_width = global_container_stack.getProperty( "machine_width", "value") machine_depth = global_container_stack.getProperty( "machine_depth", "value") arranger = Arrange.create(x=machine_width, y=machine_depth, fixed_nodes=fixed_nodes) min_offset = 8 default_extruder_position = self._application.getMachineManager( ).defaultExtruderPosition default_extruder_id = self._application._global_container_stack.extruders[ default_extruder_position].getId() select_models_on_load = self._application.getPreferences().getValue( "cura/select_models_on_load") for original_node in nodes: # Create a CuraSceneNode just if the original node is not that type if isinstance(original_node, CuraSceneNode): node = original_node else: node = CuraSceneNode() node.setMeshData(original_node.getMeshData()) #Setting meshdata does not apply scaling. if (original_node.getScale() != Vector(1.0, 1.0, 1.0)): node.scale(original_node.getScale()) node.setSelectable(True) node.setName(os.path.basename(file_name)) self._application.getBuildVolume().checkBoundsAndUpdate(node) is_non_sliceable = "." + file_extension in self._application._non_sliceable_extensions if is_non_sliceable: self._application.callLater( lambda: self._application.getController().setActiveView( "SimulationView")) block_slicing_decorator = BlockSlicingDecorator() node.addDecorator(block_slicing_decorator) else: sliceable_decorator = SliceableObjectDecorator() node.addDecorator(sliceable_decorator) scene = self._application.getController().getScene() # If there is no convex hull for the node, start calculating it and continue. if not node.getDecorator(ConvexHullDecorator): node.addDecorator(ConvexHullDecorator()) for child in node.getAllChildren(): if not child.getDecorator(ConvexHullDecorator): child.addDecorator(ConvexHullDecorator()) ### START PATCH: don't do standard arrange on load for belt printers ### but place in a line instead if is_belt_printer: half_node_depth = node.getBoundingBox().depth / 2 build_plate_empty = True leading_edge = self._application.getBuildVolume( ).getBoundingBox().front for existing_node in DepthFirstIterator(root): if (not issubclass(type(existing_node), CuraSceneNode) or (not existing_node.getMeshData() and not existing_node.callDecoration("getLayerData")) or (existing_node.callDecoration("getBuildPlateNumber") != target_build_plate)): continue build_plate_empty = False leading_edge = min(leading_edge, existing_node.getBoundingBox().back) if not build_plate_empty or leading_edge < half_node_depth: node.setPosition( Vector( 0, 0, leading_edge - half_node_depth - self._margin_between_models)) if file_extension != "3mf" and not is_belt_printer: ### END PATCH if node.callDecoration("isSliceable"): # Only check position if it's not already blatantly obvious that it won't fit. if node.getBoundingBox( ) is None or self._application._volume.getBoundingBox( ) is None or node.getBoundingBox( ).width < self._application._volume.getBoundingBox( ).width or node.getBoundingBox( ).depth < self._application._volume.getBoundingBox().depth: # Find node location offset_shape_arr, hull_shape_arr = ShapeArray.fromNode( node, min_offset=min_offset) # If a model is to small then it will not contain any points if offset_shape_arr is None and hull_shape_arr is None: Message(self._application._i18n_catalog.i18nc( "@info:status", "The selected model was too small to load."), title=self._application._i18n_catalog. i18nc("@info:title", "Warning")).show() return # Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step=10) # This node is deep copied from some other node which already has a BuildPlateDecorator, but the deepcopy # of BuildPlateDecorator produces one that's associated with build plate -1. So, here we need to check if # the BuildPlateDecorator exists or not and always set the correct build plate number. build_plate_decorator = node.getDecorator(BuildPlateDecorator) if build_plate_decorator is None: build_plate_decorator = BuildPlateDecorator(target_build_plate) node.addDecorator(build_plate_decorator) build_plate_decorator.setBuildPlateNumber(target_build_plate) op = AddSceneNodeOperation(node, scene.getRoot()) op.push() node.callDecoration("setActiveExtruder", default_extruder_id) scene.sceneChanged.emit(node) if select_models_on_load: Selection.add(node) self._application.fileCompleted.emit(file_name)