def __init__(self, *args, **kwargs): super().__init__() self._min = Vector() self._max = Vector() if len(args) == 3: self.setLeft(-args[0] / 2) self.setRight(args[0] / 2) self.setTop(args[1] / 2) self.setBottom(-args[1] / 2) self.setBack(-args[2] / 2) self.setFront(args[2] / 2) if "minimum" in kwargs: self._min = kwargs["minimum"] if "maximum" in kwargs: self._max = kwargs["maximum"] ## Sometimes the min and max are not correctly set. if not self._max: self._max = Vector() if not self._min: self._min = Vector() self._ensureMinMax()
def test_add(self): vector = Vector(0, 1, 0) vector2 = Vector(1, 0, 1) assert vector + vector2 == Vector(1, 1, 1) assert vector + vector2.getData() == Vector(1, 1, 1) vector += vector2 assert vector == Vector(1, 1, 1)
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
def compose(self, scale: Vector = None, shear: Vector = None, angles: Vector = None, translate: Vector = None, perspective: Vector = None, mirror: Vector = None) -> None: M = numpy.identity(4) if perspective is not None: P = numpy.identity(4) P[3, :] = perspective.getData()[:4] M = numpy.dot(M, P) if translate is not None: T = numpy.identity(4) T[:3, 3] = translate.getData()[:3] M = numpy.dot(M, T) if angles is not None: R = Matrix() R.setByEuler(angles.x, angles.y, angles.z, "sxyz") M = numpy.dot(M, R.getData()) if shear is not None: Z = numpy.identity(4) Z[1, 2] = shear.x Z[0, 2] = shear.y Z[0, 1] = shear.z M = numpy.dot(M, Z) if scale is not None: S = numpy.identity(4) S[0, 0] = scale.x S[1, 1] = scale.y S[2, 2] = scale.z M = numpy.dot(M, S) if mirror is not None: mir = numpy.identity(4) mir[0, 0] *= mirror.x mir[1, 1] *= mirror.y mir[2, 2] *= mirror.z M = numpy.dot(M, mir) M /= M[3, 3] self._data = M
def _rotateCamera(self, x, y): camera = self._scene.getActiveCamera() if not camera or not camera.isEnabled(): return self._scene.acquireLock() dx = math.radians(x * 180.0) dy = math.radians(y * 180.0) diff = camera.getPosition() - self._origin diff_flat = Vector(diff.x, 0.0, diff.z).getNormalized() try: new_angle = math.acos(diff_flat.dot(diff.getNormalized())) + dy except ValueError: return m = Matrix() m.setByRotationAxis(dx, Vector.Unit_Y) if new_angle < (math.pi / 2 - 0.01): m.rotateByAxis(dy, Vector.Unit_Y.cross(diff).normalize()) n = diff.multiply(m) n += self._origin camera.setPosition(n) camera.lookAt(self._origin) self._scene.releaseLock()
def test_multiply(self): vector = Vector(2, 2, 2) vector2 = Vector(2, 2, 2) assert vector * vector2 == Vector(4, 4, 4) assert vector * 2 == Vector(4, 4, 4) assert vector.scale(vector2) == Vector(4, 4, 4) vector *= vector2 assert vector == Vector(4, 4, 4)
def project(self, position: Vector) -> Tuple[float, float]: projection = self._projection_matrix view = self.getWorldTransformation() view.invert() position = position.preMultiply(view) position = position.preMultiply(projection) return position.x / position.z / 2.0, position.y / position.z / 2.0
def test_setValues(self): x = 10 y = 10 z = 10 temp_vector = Vector(x,y,z) numpy.testing.assert_array_almost_equal(temp_vector.getData(), numpy.array([x,y,z])) temp_vector2 = temp_vector.set(1, 2, 3) numpy.testing.assert_array_almost_equal(temp_vector2.getData(), numpy.array([1, 2, 3]))
def test_subtract(self): vector = Vector(2, 2, 2) vector2 = Vector(1, 1, 1) assert vector - vector2 == Vector(1, 1, 1) assert vector - vector2.getData() == Vector(1, 1, 1) assert vector2 - vector == Vector(-1, -1, -1) assert vector2 - vector.getData() == Vector(-1, -1, -1) vector -= vector2 assert vector == Vector(1, 1, 1)
def __init__(self, minimum: Vector = Vector.Null, maximum: Vector = Vector.Null) -> None: if minimum.x > maximum.x or minimum.y > maximum.y or minimum.z > maximum.z: swapped_minimum = Vector(min(minimum.x, maximum.x), min(minimum.y, maximum.y), min(minimum.z, maximum.z)) swapped_maximum = Vector(max(minimum.x, maximum.x), max(minimum.y, maximum.y), max(minimum.z, maximum.z)) minimum = swapped_minimum maximum = swapped_maximum minimum.setRoundDigits(3) maximum.setRoundDigits(3) self._min = minimum #type: Vector self._max = maximum #type: Vector
def __init__(self, *args, **kwargs): self._position = Vector() self._normal = None if len(args) == 3: self._position = Vector(args[0], args[1], args[2]) if "position" in kwargs: self._position = kwargs["position"] if "normal" in kwargs: self._normal = kwargs["normal"]
class Vertex(object): ## Construct a vertex. # # Possible keyword parameters: # - position: Vector with the vertex' position. # - normal: Vector with the vertex' normal # # Unnamed arguments: # - x, y, z passed as numbers. def __init__(self, *args, **kwargs): self._position = Vector() self._normal = None if len(args) == 3: self._position = Vector(args[0], args[1], args[2]) if "position" in kwargs: self._position = kwargs["position"] if "normal" in kwargs: self._normal = kwargs["normal"] ## Get the position the vertex # \returns position Vector @property def position(self): return self._position ## Set the position the vertex # \param position Vector def setPosition(self, position): self._position = position ## Get the normal the vertex # \returns normal Vector @property def normal(self): return self._normal ## Set the normal the vertex # \param normal Vector def setNormal(self, normal): self._normal = normal def hasNormal(self): return self._normal != None ## Convert the vertex into a string, which is required for parsing over sockets / streams # It's kinda hackish to do it this way, but it would take to much effort to implement myself. def toString(self): return numpy.array([self._position.x(),self._position.y(),self._position.z(),self._normal.x(),self._normal.y(),self._normal.z()]).tostring()
def __init__(self, vertices=None, normals=None, indices=None, colors=None, uvs=None, file_name=None, center_position=None, zero_position=None, type = MeshType.faces, attributes=None): self._application = None # Initialize this later otherwise unit tests break self._vertices = NumPyUtil.immutableNDArray(vertices) self._normals = NumPyUtil.immutableNDArray(normals) self._indices = NumPyUtil.immutableNDArray(indices) self._colors = NumPyUtil.immutableNDArray(colors) self._uvs = NumPyUtil.immutableNDArray(uvs) self._vertex_count = len(self._vertices) if self._vertices is not None else 0 self._face_count = len(self._indices) if self._indices is not None else 0 self._type = type self._file_name = file_name # type: Optional[str] # original center position self._center_position = center_position # original zero position, is changed after transformation if zero_position is not None: self._zero_position = zero_position else: self._zero_position = Vector(0, 0, 0) # type: Vector self._convex_hull = None # type: Optional[scipy.spatial.ConvexHull] self._convex_hull_vertices = None # type: Optional[numpy.ndarray] self._convex_hull_lock = threading.Lock() self._attributes = {} if attributes is not None: for key, attribute in attributes.items(): new_value = {} for attribute_key, attribute_value in attribute.items(): if attribute_key == "value": new_value["value"] = NumPyUtil.immutableNDArray(attribute_value) else: new_value[attribute_key] = attribute_value self._attributes[key] = new_value
def __init__(self, parent=None): super().__init__() # Call super to make multiple inheritence work. self._children = [] self._mesh_data = None self._position = Vector() self._scale = Vector(1.0, 1.0, 1.0) self._orientation = Quaternion() self._transformation = None self._world_transformation = None self._derived_position = None self._derived_orientation = None self._derived_scale = None self._inherit_orientation = True self._inherit_scale = True self._parent = parent self._enabled = True self._selectable = False self._calculate_aabb = True self._aabb = None self._aabb_job = None self._visible = True self._name = "" self._decorators = [] self._bounding_box_mesh = None self.boundingBoxChanged.connect(self.calculateBoundingBoxMesh) self.parentChanged.connect(self._onParentChanged) if parent: parent.addChild(self)
def __init__(self, parent = None): super().__init__() # Call super to make multiple inheritence work. self._children = [] self._mesh_data = None self._position = Vector() self._scale = Vector(1.0, 1.0, 1.0) self._orientation = Quaternion() self._transformation = None self._world_transformation = None self._derived_position = None self._derived_orientation = None self._derived_scale = None self._inherit_orientation = True self._inherit_scale = True self._parent = parent self._enabled = True self._selectable = False self._calculate_aabb = True self._aabb = None self._aabb_job = None self._visible = True self._name = "" if parent: parent.addChild(self)
def read(self, file_name, **kwargs): try: for id, reader in self._mesh_readers.items(): result = reader.read(file_name) if result is not None: if kwargs.get("center", True): # If the result has a mesh and no children it needs to be centered if result.getMeshData() and len(result.getChildren()) == 0: extents = result.getMeshData().getExtents() move_vector = Vector() move_vector.setX(extents.center.x) move_vector.setY(extents.center.y) # Ensure that bottom is on 0 (above plate) move_vector.setZ(extents.center.z) result.setCenterPosition(move_vector) if result.getMeshData().getExtents().bottom != 0: result.translate(Vector(0,-result.getMeshData().getExtents().bottom ,0)) # Move all the meshes of children so that toolhandles are shown in the correct place. for node in result.getChildren(): if node.getMeshData(): extents = node.getMeshData().getExtents() m = Matrix() m.translate(-extents.center) node.setMeshData(node.getMeshData().getTransformed(m)) node.translate(extents.center) return result except OSError as e: Logger.log("e", str(e)) Logger.log("w", "Unable to read file %s", file_name) return None #unable to read
def __imul__(self, other): if type(other) is Quaternion: v1 = Vector(other.x, other.y, other.z) v2 = Vector(self.x, self.y, self.z) w = other.w * self.w - v1.dot(v2) v = v2 * other.w + v1 * self.w + v2.cross(v1) self._data[0] = v.x self._data[1] = v.y self._data[2] = v.z self._data[3] = w elif type(other) is float or type(other) is int: self._data *= other else: raise NotImplementedError() return self
def _onChangeTimerFinished(self): root = self._controller.getScene().getRoot() for node in BreadthFirstIterator(root): if node is root or type(node) is not SceneNode: continue bbox = node.getBoundingBox() if not bbox or not bbox.isValid(): continue # Mark the node as outside the build volume if the bounding box test fails. if self._build_volume.getBoundingBox().intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection: node._outside_buildarea = True else: node._outside_buildarea = False # Move the node upwards if the bottom is below the build platform. move_vector = Vector() if not Float.fuzzyCompare(bbox.bottom, 0.0): move_vector.setY(-bbox.bottom) # If there is no convex hull for the node, start calculating it and continue. if not hasattr(node, "_convex_hull"): if not hasattr(node, "_convex_hull_job"): job = ConvexHullJob.ConvexHullJob(node) job.start() node._convex_hull_job = job elif Selection.isSelected(node): pass else: # 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 nodes that do not have the right properties set. if not hasattr(other_node, "_convex_hull") or not other_node.getBoundingBox(): continue # Check to see if the bounding boxes intersect. If not, we can ignore the node as there is no way the hull intersects. if node.getBoundingBox().intersectsBox(other_node.getBoundingBox()) == AxisAlignedBox.IntersectionResult.NoIntersection: continue # Get the overlap distance for both convex hulls. If this returns None, there is no intersection. overlap = node._convex_hull.intersectsPolygon(other_node._convex_hull) if overlap is None: continue move_vector.setX(overlap[0] * 1.1) move_vector.setZ(overlap[1] * 1.1) if move_vector != Vector(): op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector) op.push() if node.getBoundingBox().intersectsBox(self._build_volume.getBoundingBox()) == AxisAlignedBox.IntersectionResult.FullIntersection: op = ScaleToBoundsOperation(node, self._build_volume.getBoundingBox()) op.push()
def _zoomCamera(self, zoom_range: float, event: Optional[Event] = None) -> None: camera = self._scene.getActiveCamera() if not camera or not camera.isEnabled(): return self.clipToZoom() self._scene.getSceneLock().acquire() r = (camera.getWorldPosition() - self._origin).length() delta = r * (zoom_range / 128 / 10.0) r -= delta if self._invert_zoom: delta *= -1 move_vector = Vector(0.0, 0.0, 1.0) if event is not None and self._zoom_to_mouse: viewport_center_x = QtApplication.getInstance().getRenderer().getViewportWidth() / 2 viewport_center_y = QtApplication.getInstance().getRenderer().getViewportHeight() / 2 main_window = cast(MainWindow, QtApplication.getInstance().getMainWindow()) mouse_diff_center_x = viewport_center_x - main_window.mouseX mouse_diff_center_y = viewport_center_y - main_window.mouseY x_component = mouse_diff_center_x / QtApplication.getInstance().getRenderer().getViewportWidth() y_component = mouse_diff_center_y / QtApplication.getInstance().getRenderer().getViewportHeight() move_vector = Vector(x_component, -y_component, 1) move_vector = move_vector.normalized() move_vector = -delta * move_vector if delta != 0: if self._min_zoom < r < self._max_zoom: camera.translate(move_vector) if self._zoom_to_mouse: # Set the origin of the camera to the new distance, right in front of the new camera position. self._origin = (r * Vector(0.0, 0.0, -1.0)).preMultiply(camera.getWorldTransformation()) self._scene.getSceneLock().release()
class TestVector(unittest.TestCase): def setUp(self): # Called before the first testfunction is executed self._vector = Vector(1, 0, 0) pass def tearDown(self): # Called after the last testfunction was executed pass def test_getData(self): numpy.testing.assert_array_almost_equal(self._vector.getData(), numpy.array([1, 0, 0])) pass def test_angleBetweenVectors(self): second_vector = Vector(1, 0, 0) third_vector = Vector(0, 1, 0) fourth_vector = Vector(0, 0, 1) # Check if angle with itself is 0 self.assertEqual(self._vector.angleToVector(second_vector), 0) # Check if angle between the two vectors that are rotated in equal angle but different direction are the same self.assertEqual(self._vector.angleToVector(third_vector), self._vector.angleToVector(fourth_vector)) def test_normalize(self): pass def test_setValues(self): x = 10 y = 10 z = 10 temp_vector = Vector(x, y, z) numpy.testing.assert_array_almost_equal(temp_vector.getData(), numpy.array([x, y, z])) def test_negPos(self): v = Vector(0, 1, 0) self.assertEqual(Vector(0, -1, 0), -v) self.assertEqual(Vector(0, 1, 0), v) # - should have no side effects
def findOuterNormal(face): n = len(face) for i in range(n): for j in range(i+1, n): edge = face[j] - face[i] if edge.length() > EPSILON: edge = edge.normalized() prev_rejection = Vector() is_outer = True for k in range(n): if k != i and k != j: pt = face[k] - face[i] pte = pt.dot(edge) rejection = pt - edge*pte if rejection.dot(prev_rejection) < -EPSILON: # points on both sides of the edge - not an outer one is_outer = False break elif rejection.length() > prev_rejection.length(): # Pick a greater rejection for numeric stability prev_rejection = rejection if is_outer: # Found an outer edge, prev_rejection is the rejection inside the face. Generate a normal. return edge.cross(prev_rejection) return False
def setByRotationAxis(self, angle: float, direction: Vector, point: Optional[List[float]] = None) -> None: sina = math.sin(angle) cosa = math.cos(angle) direction_data = self._unitVector(direction.getData()) # rotation matrix around unit vector R = numpy.diag([cosa, cosa, cosa]) R += numpy.outer(direction_data, direction_data) * (1.0 - cosa) direction_data *= sina R += numpy.array([[ 0.0, -direction_data[2], direction_data[1]], [ direction_data[2], 0.0, -direction_data[0]], [-direction_data[1], direction_data[0], 0.0]], dtype = numpy.float64) M = numpy.identity(4) M[:3, :3] = R if point is not None: # rotation not around origin point = numpy.array(point[:3], dtype = numpy.float64, copy=False) M[:3, 3] = point - numpy.dot(R, point) self._data = M
def lookAt(self, target: Vector, up: Vector = Vector.Unit_Y): if not self._enabled: return eye = self.getWorldPosition() f = (target - eye).normalized() up = up.normalized() s = f.cross(up).normalized() u = s.cross(f).normalized() m = Matrix([ [ s.x, u.x, -f.x, 0.0], [ s.y, u.y, -f.y, 0.0], [ s.z, u.z, -f.z, 0.0], [ 0.0, 0.0, 0.0, 1.0] ]) self.setOrientation(Quaternion.fromMatrix(m))
def processGCodeStream(self, stream: str, filename: str) -> Optional["CuraSceneNode"]: Logger.log("d", "Preparing to load GCode") self._cancelled = False # We obtain the filament diameter from the selected extruder to calculate line widths global_stack = CuraApplication.getInstance().getGlobalContainerStack() if not global_stack: return None self._filament_diameter = global_stack.extruders[str( self._extruder_number)].getProperty("material_diameter", "value") scene_node = CuraSceneNode() gcode_list = [] self._is_layers_in_file = False self._extruder_offsets = self._extruderOffsets( ) # dict with index the extruder number. can be empty ############################################################################################## ## This part is where the action starts ############################################################################################## file_lines = 0 current_line = 0 for line in stream.split("\n"): file_lines += 1 gcode_list.append(line + "\n") if not self._is_layers_in_file and line[:len( self._layer_keyword)] == self._layer_keyword: self._is_layers_in_file = True file_step = max(math.floor(file_lines / 100), 1) self._clearValues() self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"), lifetime=0, title=catalog.i18nc("@info:title", "G-code Details")) assert (self._message is not None) # use for typing purposes self._message.setProgress(0) self._message.show() Logger.log("d", "Parsing Gcode...") current_position = Position(0, 0, 0, 0, [0]) current_path = [] #type: List[List[float]] min_layer_number = 0 negative_layers = 0 previous_layer = 0 self._previous_extrusion_value = 0.0 for line in stream.split("\n"): if self._cancelled: Logger.log("d", "Parsing Gcode file cancelled") return None current_line += 1 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 elif type == "SUPPORT-INTERFACE": self._layer_type = LayerPolygon.SupportInterfaceType elif type == "PRIME-TOWER": self._layer_type = LayerPolygon.PrimeTowerType else: Logger.log( "w", "Encountered a unknown type (%s) while parsing g-code.", type) # When the layer change is reached, the polygon is computed so we have just one layer per extruder 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() # Start the new layer at the end position of the last layer current_path.append([ current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType ]) # When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior # as in ProcessSlicedLayersJob if layer_number < min_layer_number: min_layer_number = layer_number if layer_number < 0: layer_number += abs(min_layer_number) negative_layers += 1 else: layer_number += negative_layers # In case there is a gap in the layer count, empty layers are created for empty_layer in range(previous_layer + 1, layer_number): self._createEmptyLayer(empty_layer) self._layer_number = layer_number previous_layer = 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: # When find a movement, the new posistion is calculated and added to the current_path, but # don't need to create a polygon until the end of the layer current_position = self.processGCode(G, line, current_position, current_path) continue # When changing the extruder, the polygon with the stored paths is computed if line.startswith("T"): T = self._getInt(line, "T") if T is not None: self._extruders_seen.add(T) self._createPolygon( self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])) current_path.clear() # When changing tool, store the end point of the previous path, then process the code and finally # add another point with the new position of the head. current_path.append([ current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType ]) current_position = self.processTCode( T, line, current_position, current_path) current_path.append([ current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType ]) if line.startswith("M"): M = self._getInt(line, "M") if M is not None: self.processMCode(M, line, current_position, current_path) # "Flush" leftovers. Last layer paths are still stored if 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((8, 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] material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0] material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0] material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0] material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0] material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0] material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0] layer_mesh = self._layer_data_builder.build(material_color_map) decorator = LayerDataDecorator() decorator.setLayerData(layer_mesh) scene_node.addDecorator(decorator) gcode_list_decorator = GCodeListDecorator() gcode_list_decorator.setGcodeFileName(filename) gcode_list_decorator.setGCodeList(gcode_list) scene_node.addDecorator(gcode_list_decorator) # gcode_dict stores gcode_lists for a number of build plates. active_build_plate_id = CuraApplication.getInstance( ).getMultiBuildPlateModel().activeBuildPlate gcode_dict = {active_build_plate_id: gcode_list} CuraApplication.getInstance().getController().getScene( ).gcode_dict = gcode_dict #type: ignore #Because gcode_dict is generated dynamically. Logger.log("d", "Finished parsing Gcode") self._message.hide() if self._layer_number == 0: Logger.log("w", "File doesn't contain any valid layers") if not global_stack.getProperty("machine_center_is_zero", "value"): machine_width = global_stack.getProperty("machine_width", "value") machine_depth = global_stack.getProperty("machine_depth", "value") scene_node.setPosition( Vector(-machine_width / 2, 0, machine_depth / 2)) Logger.log("d", "GCode loading finished") if CuraApplication.getInstance().getPreferences().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, title=catalog.i18nc( "@info:title", "G-code Details")) caution_message.show() # The "save/print" button's state is bound to the backend state. backend = CuraApplication.getInstance().getBackend() backend.backendStateChange.emit(Backend.BackendState.Disabled) return scene_node
def snapshot(width=300, height=300): scene = Application.getInstance().getController().getScene() active_camera = scene.getActiveCamera() render_width, render_height = active_camera.getWindowSize() render_width = int(render_width) render_height = int(render_height) preview_pass = PreviewPass(render_width, render_height) root = scene.getRoot() camera = Camera("snapshot", root) # determine zoom and look at bbox = None for node in DepthFirstIterator(root): if not getattr(node, "_outside_buildarea", False): if node.callDecoration("isSliceable") and node.getMeshData( ) and node.isVisible( ) and not node.callDecoration("isNonThumbnailVisibleMesh"): if bbox is None: bbox = node.getBoundingBox() else: bbox = bbox + node.getBoundingBox() # If there is no bounding box, it means that there is no model in the buildplate if bbox is None: return None look_at = bbox.center # guessed size so the objects are hopefully big size = max(bbox.width, bbox.height, bbox.depth * 0.5) # Looking from this direction (x, y, z) in OGL coordinates looking_from_offset = Vector(-1, 1, 2) if size > 0: # determine the watch distance depending on the size looking_from_offset = looking_from_offset * size * 1.3 camera.setPosition(look_at + looking_from_offset) camera.lookAt(look_at) satisfied = False size = None fovy = 30 while not satisfied: if size is not None: satisfied = True # always be satisfied after second try projection_matrix = Matrix() # Somehow the aspect ratio is also influenced in reverse by the screen width/height # So you have to set it to render_width/render_height to get 1 projection_matrix.setPerspective(fovy, render_width / render_height, 1, 500) camera.setProjectionMatrix(projection_matrix) preview_pass.setCamera(camera) preview_pass.render() pixel_output = preview_pass.getOutput() min_x, max_x, min_y, max_y = Snapshot.getImageBoundaries( pixel_output) size = max((max_x - min_x) / render_width, (max_y - min_y) / render_height) if size > 0.5 or satisfied: satisfied = True else: # make it big and allow for some empty space around fovy *= 0.5 # strangely enough this messes up the aspect ratio: fovy *= size * 1.1 # make it a square if max_x - min_x >= max_y - min_y: # make y bigger min_y, max_y = int((max_y + min_y) / 2 - (max_x - min_x) / 2), int((max_y + min_y) / 2 + (max_x - min_x) / 2) else: # make x bigger min_x, max_x = int((max_x + min_x) / 2 - (max_y - min_y) / 2), int((max_x + min_x) / 2 + (max_y - min_y) / 2) cropped_image = pixel_output.copy(min_x, min_y, max_x - min_x, max_y - min_y) # Scale it to the correct size scaled_image = cropped_image.scaled( width, height, aspectRatioMode=QtCore.Qt.IgnoreAspectRatio, transformMode=QtCore.Qt.SmoothTransformation) return scaled_image
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 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._snap_rotation = (not self._snap_rotation) self.propertyChanged.emit() if event.type == Event.KeyReleaseEvent and event.key == KeyEvent.ShiftKey: # Snap is "toggled back" when releasing the shift button self._snap_rotation = (not self._snap_rotation) self.propertyChanged.emit() 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 Selection.getAllSelectedObjects(): 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 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._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 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 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 op = GroupedOperation() for node, position in self._saved_node_positions: op.addOperation( RotateOperation(node, rotation, rotate_around_point=position)) op.push() self.setDragStart(event.x, event.y) if event.type == Event.MouseReleaseEvent: # Finish a rotate operation if self.getDragPlane(): self.setDragPlane(None) self.setLockedAxis(None) self._angle = None self.propertyChanged.emit() if self._rotating: self.operationStopped.emit(self) return True
def getTranslation(self) -> Vector: return Vector(data=self._data[:3, 3])
def test_translateWorld(self): node1 = SceneNode() node2 = SceneNode(node1) self.assertEqual(node2.getWorldPosition(), Vector(0, 0, 0)) node1.translate(Vector(0, 0, 10)) self.assertEqual(node1.getWorldPosition(), Vector(0, 0, 10)) self.assertEqual(node2.getWorldPosition(), Vector(0, 0, 10)) node2.translate(Vector(0, 0, 10)) self.assertEqual(node1.getWorldPosition(), Vector(0, 0, 10)) self.assertEqual(node2.getWorldPosition(), Vector(0, 0, 20)) node1.rotate(Quaternion.fromAngleAxis(math.pi / 2, Vector.Unit_Y)) self.assertEqual(node1.getWorldPosition(), Vector(0, 0, 10)) self.assertEqual(node2.getWorldPosition(), Vector(10, 0, 10)) node2.translate(Vector(0, 0, 10)) # Local translation on Z with a parent rotated 90 degrees results in movement on X axis pos = node2.getWorldPosition() #Using fuzzyCompare due to accumulation of floating point error self.assertTrue( Float.fuzzyCompare(pos.x, 20, 1e-5), "{0} does not equal {1}".format(pos, Vector(20, 0, 10))) self.assertTrue( Float.fuzzyCompare(pos.y, 0, 1e-5), "{0} does not equal {1}".format(pos, Vector(20, 0, 10))) self.assertTrue( Float.fuzzyCompare(pos.z, 10, 1e-5), "{0} does not equal {1}".format(pos, Vector(20, 0, 10))) node2.translate(Vector(0, 0, 10), SceneNode.TransformSpace.World) # World translation on Z with a parent rotated 90 degrees results in movement on Z axis pos = node2.getWorldPosition() self.assertTrue( Float.fuzzyCompare(pos.x, 20, 1e-5), "{0} does not equal {1}".format(pos, Vector(20, 0, 20))) self.assertTrue( Float.fuzzyCompare(pos.y, 0, 1e-5), "{0} does not equal {1}".format(pos, Vector(20, 0, 20))) self.assertTrue( Float.fuzzyCompare(pos.z, 20, 1e-5), "{0} does not equal {1}".format(pos, Vector(20, 0, 20))) node1.translate(Vector(0, 0, 10)) self.assertEqual(node1.getWorldPosition(), Vector(10, 0, 10)) pos = node2.getWorldPosition() self.assertTrue( Float.fuzzyCompare(pos.x, 30, 1e-5), "{0} does not equal {1}".format(pos, Vector(30, 0, 20))) self.assertTrue( Float.fuzzyCompare(pos.y, 0, 1e-5), "{0} does not equal {1}".format(pos, Vector(30, 0, 20))) self.assertTrue( Float.fuzzyCompare(pos.z, 20, 1e-5), "{0} does not equal {1}".format(pos, Vector(30, 0, 20))) node1.scale(Vector(2, 2, 2)) pos = node2.getWorldPosition() self.assertTrue( Float.fuzzyCompare(pos.x, 50, 1e-4), "{0} does not equal {1}".format(pos, Vector(50, 0, 30))) self.assertTrue( Float.fuzzyCompare(pos.y, 0, 1e-4), "{0} does not equal {1}".format(pos, Vector(50, 0, 30))) self.assertTrue( Float.fuzzyCompare(pos.z, 30, 1e-4), "{0} does not equal {1}".format(pos, Vector(50, 0, 30))) node2.translate(Vector(0, 0, 10)) pos = node2.getWorldPosition() self.assertTrue( Float.fuzzyCompare(pos.x, 70, 1e-4), "{0} does not equal {1}".format(pos, Vector(70, 0, 30))) self.assertTrue( Float.fuzzyCompare(pos.y, 0, 1e-4), "{0} does not equal {1}".format(pos, Vector(70, 0, 30))) self.assertTrue( Float.fuzzyCompare(pos.z, 30, 1e-4), "{0} does not equal {1}".format(pos, Vector(70, 0, 30))) # World space set position node1 = SceneNode() node2 = SceneNode(node1) node1.setPosition(Vector(15, 15, 15)) node2.setPosition(Vector(10, 10, 10)) self.assertEqual(node2.getWorldPosition(), Vector(25, 25, 25)) #node2.setPosition(Vector(15,15,15), SceneNode.TransformSpace.World) #self.assertEqual(node2.getWorldPosition(), Vector(15, 15, 15)) #self.assertEqual(node2.getPosition(), Vector(0,0,0)) node1.setPosition(Vector(15, 15, 15)) node2.setPosition(Vector(0, 0, 0)) node2.rotate(Quaternion.fromAngleAxis(-math.pi / 2, Vector.Unit_Y)) node2.translate(Vector(10, 0, 0)) self.assertEqual(node2.getWorldPosition(), Vector(15, 15, 25)) node2.setPosition(Vector(15, 15, 25), SceneNode.TransformSpace.World) self.assertEqual(node2.getWorldPosition(), Vector(15, 15, 25)) self.assertEqual(node2.getPosition(), Vector(0, 0, 10))
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 disabling the remove empty first layers setting, the minimum layer number will be a positive # value. In that case the first empty layers will be discarded and start processing layers from the # first layer with data. # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we # 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 = sys.maxsize negative_layers = 0 for layer in self._layers: if layer.repeatedMessageCount("path_segment") > 0: 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: # If the layer is below the minimum, it means that there is no data, so that we don't create a layer # data. However, if there are empty layers in between, we compute them. if layer.id < min_layer_number: continue # Layers are offset by the minimum layer number. In case the raft (negative layers) is being used, # then the absolute layer number is adjusted by removing the empty layers that can be in between raft # and the model abs_layer_number = layer.id - min_layer_number if layer.id >= 0 and negative_layers != 0: abs_layer_number += (min_layer_number + 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 = manager.getActiveExtruderStacks() if extruders: material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32) for extruder in extruders: position = int( extruder.getMetaDataEntry("position", default="0")) 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 updateCurrentValue(self, value): self._camera_tool.setOrigin(Vector(value.x(), value.y(), value.z()))
def run(self): start_time = time() 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) new_node = SceneNode() ## Remove old layer data (if any) for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("getLayerData"): node.getParent().removeChild(node) break if self._abort_requested: if self._progress: self._progress.hide() return # 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. min_layer_number = 0 for layer in self._layers: if layer.id < min_layer_number: min_layer_number = layer.id current_layer = 0 for layer in self._layers: abs_layer_number = layer.id + abs(min_layer_number) layer_data.addLayer(abs_layer_number) this_layer = layer_data.getLayer(abs_layer_number) layer_data.setLayerHeight(abs_layer_number, layer.height) 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. # In the future, line_thicknesses should be given by CuraEngine as well. # Currently the infill layer thickness also translates to line width line_thicknesses = numpy.zeros(line_widths.shape, dtype="f4") line_thicknesses[:] = layer.thickness / 1000 # from micrometer to millimeter # 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) 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: 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 # 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(Preferences.getInstance( ).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: self._progress.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: self._progress.setProgress(100) view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "LayerView": view.resetLayerData() if self._progress: self._progress.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 processCliStream(self, stream: str) -> Optional[SteSlicerSceneNode]: Logger.log("d", "Preparing to load CLI") self._cancelled = False self._setPrintSettings() self._is_layers_in_file = False scene_node = SteSlicerSceneNode() gcode_list = [] self._writeStartCode(gcode_list) gcode_list.append(";LAYER_COUNT\n") # Reading starts here file_lines = 0 current_line = 0 for line in stream.split("\n"): file_lines += 1 if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword: self._is_layers_in_file = True file_step = max(math.floor(file_lines / 100), 1) self._clearValues() self._message = Message(catalog.i18nc("@info:status", "Parsing CLI"), lifetime=0, title=catalog.i18nc("@info:title", "CLI Details")) assert(self._message is not None) # use for typing purposes self._message.setProgress(0) self._message.show() Logger.log("d", "Parsing CLI...") self._position = Position(0, 0, 0, 0, 0, 1, 0, [0]) self._gcode_position = Position(0, 0, 0, 0, 0, 0, 0, [0]) current_path = [] # type: List[List[float]] geometry_start = False for line in stream.split("\n"): if self._cancelled: Logger.log("d", "Parsing CLI file cancelled") return None current_line += 1 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 == "$$GEOMETRYSTART": geometry_start = True continue if not geometry_start: continue if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword: try: layer_height = float(line[len(self._layer_keyword):]) self._current_layer_thickness = layer_height - self._current_layer_height if self._current_layer_thickness > 0.4: self._current_layer_thickness = 0.2 self._current_layer_height = layer_height self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get( self._extruder_number, [0, 0])) current_path.clear() # Start the new layer at the end position of the last layer current_path.append([self._position.x, self._position.y, self._position.z, self._position.a, self._position.b, self._position.c, self._position.f, self._position.e[self._extruder_number], LayerPolygon.MoveCombingType]) self._layer_number += 1 gcode_list.append(";LAYER:%s\n" % self._layer_number) except: pass if line.find(self._body_type_keyword) == 0: self._layer_type = LayerPolygon.Inset0Type if line.find(self._support_type_keyword) == 0: self._layer_type = LayerPolygon.SupportType if line.find(self._perimeter_type_keyword) == 0: self._layer_type = LayerPolygon.Inset0Type if line.find(self._skin_type_keyword) == 0: self._layer_type = LayerPolygon.SkinType if line.find(self._infill_type_keyword) == 0: self._layer_type = LayerPolygon.InfillType # Comment line if line.startswith("//"): continue # Polyline processing self.processPolyline(line, current_path, gcode_list) # "Flush" leftovers. Last layer paths are still stored if 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() layer_count_idx = gcode_list.index(";LAYER_COUNT\n") if layer_count_idx > 0: gcode_list[layer_count_idx] = ";LAYER_COUNT:%s\n" % self._layer_number end_gcode = self._global_stack.getProperty( "machine_end_gcode", "value") gcode_list.append(end_gcode + "\n") material_color_map = numpy.zeros((8, 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] material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0] material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0] material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0] material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0] material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0] material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0] layer_mesh = self._layer_data_builder.build(material_color_map) decorator = 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) # gcode_dict stores gcode_lists for a number of build plates. active_build_plate_id = SteSlicerApplication.getInstance( ).getMultiBuildPlateModel().activeBuildPlate gcode_dict = {active_build_plate_id: gcode_list} # type: ignore #Because gcode_dict is generated dynamically. SteSlicerApplication.getInstance().getController().getScene().gcode_dict = gcode_dict Logger.log("d", "Finished parsing CLI file") self._message.hide() if self._layer_number == 0: Logger.log("w", "File doesn't contain any valid layers") if not self._global_stack.getProperty("machine_center_is_zero", "value"): machine_width = self._global_stack.getProperty( "machine_width", "value") machine_depth = self._global_stack.getProperty( "machine_depth", "value") scene_node.setPosition( Vector(-machine_width / 2, 0, machine_depth / 2)) Logger.log("d", "CLI loading finished") if SteSlicerApplication.getInstance().getPreferences().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, title=catalog.i18nc("@info:title", "G-code Details")) caution_message.show() backend = SteSlicerApplication.getInstance().getBackend() backend.backendStateChange.emit(Backend.BackendState.Disabled) return scene_node
def _onChangeTimerFinished(self): if not self._enabled: return root = self._controller.getScene().getRoot() for node in BreadthFirstIterator(root): if node is root or type(node) is not SceneNode: continue bbox = node.getBoundingBox() if not bbox or not bbox.isValid(): self._change_timer.start() continue build_volume_bounding_box = copy.deepcopy(self._build_volume.getBoundingBox()) build_volume_bounding_box.setBottom(-9001) # Ignore intersections with the bottom 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 else: # When printing one at a time too high objects are not printable. if Application.getInstance().getMachineManager().getWorkingProfile().getSettingValue("print_sequence") == "one_at_a_time": if node.getBoundingBox().height > Application.getInstance().getMachineManager().getWorkingProfile().getSettingValue("gantry_height"): node._outside_buildarea = True # Move it downwards if bottom is above platform move_vector = Vector() if not (node.getParent() and node.getParent().callDecoration("isGroup")): #If an object is grouped, don't move it down z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0 if bbox.bottom > 0: move_vector.setY(-bbox.bottom + z_offset) elif bbox.bottom < z_offset: move_vector.setY((-bbox.bottom) - z_offset) #if not Float.fuzzyCompare(bbox.bottom, 0.0): # pass#move_vector.setY(-bbox.bottom) # If there is no convex hull for the node, start calculating it and continue. if not node.getDecorator(ConvexHullDecorator): node.addDecorator(ConvexHullDecorator()) if not node.callDecoration("getConvexHull"): if not node.callDecoration("getConvexHullJob"): job = ConvexHullJob.ConvexHullJob(node) job.start() node.callDecoration("setConvexHullJob", job) elif Preferences.getInstance().getValue("physics/automatic_push_free"): # 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 colissions of a group with it's own children if other_node in node.getAllChildren() or node in other_node.getAllChildren(): continue # Ignore colissions within a group if other_node.getParent().callDecoration("isGroup") is not None or node.getParent().callDecoration("isGroup") is not None: continue #if node.getParent().callDecoration("isGroup") is other_node.getParent().callDecoration("isGroup"): # continue # Ignore nodes that do not have the right properties set. if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox(): continue # Check to see if the bounding boxes intersect. If not, we can ignore the node as there is no way the hull intersects. #if node.getBoundingBox().intersectsBox(other_node.getBoundingBox()) == AxisAlignedBox.IntersectionResult.NoIntersection: # continue # Get the overlap distance for both convex hulls. If this returns None, there is no intersection. try: head_hull = node.callDecoration("getConvexHullHead") if head_hull: overlap = head_hull.intersectsPolygon(other_node.callDecoration("getConvexHull")) if not overlap: other_head_hull = other_node.callDecoration("getConvexHullHead") if other_head_hull: overlap = node.callDecoration("getConvexHull").intersectsPolygon(other_head_hull) else: overlap = node.callDecoration("getConvexHull").intersectsPolygon(other_node.callDecoration("getConvexHull")) except: overlap = None #It can sometimes occur that the calculated convex hull has no size, in which case there is no overlap. if overlap is None: continue move_vector.setX(overlap[0] * 1.1) move_vector.setZ(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 move_vector != Vector(): op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector) op.push()
def addPyramid(self, width, height, depth, angle=0, axis=Vector.Unit_Y, center=Vector(0, 0, 0), color=None): """Adds a pyramid to the mesh of this mesh builder. :param width: The width of the base of the pyramid. :param height: The height of the pyramid (from base to notch). :param depth: The depth of the base of the pyramid. :param angle: (Optional) An angle of rotation to rotate the pyramid by, in degrees. :param axis: (Optional) An axis of rotation to rotate the pyramid around. If no axis is provided and the angle of rotation is nonzero, the pyramid will be rotated around the Y-axis. :param center: (Optional) The position of the centre of the base of the pyramid. If not provided, the pyramid will be placed on the coordinate origin. :param color: (Optional) The colour of the pyramid. If no colour is provided, a colour will be determined by the shader. """ angle = math.radians(angle) minW = -width / 2 maxW = width / 2 minD = -depth / 2 maxD = depth / 2 start = self.getVertexCount() #Starting index. matrix = Matrix() matrix.setByRotationAxis(angle, axis) verts = numpy.asarray( [ #All 5 vertices of the pyramid. [minW, 0, maxD], [maxW, 0, maxD], [minW, 0, minD], [maxW, 0, minD], [0, height, 0] ], dtype=numpy.float32) verts = verts.dot( matrix.getData()[0:3, 0:3]) #Rotate the pyramid around the axis. verts[:] += center.getData() self.addVertices(verts) indices = numpy.asarray( [ #Connect the vertices to each other (6 triangles). [start, start + 1, start + 4], #The four sides of the pyramid. [start + 1, start + 3, start + 4], [start + 3, start + 2, start + 4], [start + 2, start, start + 4], [start, start + 3, start + 1], #The base of the pyramid. [start, start + 2, start + 3] ], dtype=numpy.int32) self.addIndices(indices) if color: #If we have a colour, add the colour to each of the vertices. vertex_count = self.getVertexCount() for i in range(1, 6): self.setVertexColor(vertex_count - i, color)
def addDonut(self, inner_radius, outer_radius, width, center=Vector(0, 0, 0), sections=32, color=None, angle=0, axis=Vector.Unit_Y): """Adds a torus to the mesh of this mesh builder. The torus is the shape of a doughnut. This doughnut is delicious and moist, but not very healthy. :param inner_radius: The radius of the hole inside the torus. Must be smaller than outer_radius. :param outer_radius: The radius of the outside of the torus. Must be larger than inner_radius. :param width: The radius of the torus in perpendicular direction to its perimeter. This is the "thickness". :param center: (Optional) The position of the centre of the torus. If no position is provided, the torus will be centred around the coordinate origin. :param sections: (Optional) The resolution of the torus in the circumference. The resolution of the intersection of the torus cannot be changed. :param color: (Optional) The colour of the torus. If no colour is provided, a colour will be determined by the shader. :param angle: (Optional) An angle of rotation to rotate the torus by, in radians. :param axis: (Optional) An axis of rotation to rotate the torus around. If no axis is provided and the angle of rotation is nonzero, the torus will be rotated around the Y-axis. """ vertices = [] indices = [] colors = [] start = self.getVertexCount() #Starting index. for i in range(sections): v1 = start + i * 3 #Indices for each of the vertices we'll add for this section. v2 = v1 + 1 v3 = v1 + 2 v4 = v1 + 3 v5 = v1 + 4 v6 = v1 + 5 if i + 1 >= sections: # connect the end to the start v4 = start v5 = start + 1 v6 = start + 2 theta = i * math.pi / ( sections / 2) #Angle of this piece around torus perimeter. c = math.cos(theta) #X-coordinate around torus perimeter. s = math.sin(theta) #Y-coordinate around torus perimeter. #One vertex on the inside perimeter, two on the outside perimiter (up and down). vertices.append([inner_radius * c, inner_radius * s, 0]) vertices.append([outer_radius * c, outer_radius * s, width]) vertices.append([outer_radius * c, outer_radius * s, -width]) #Connect the vertices to the next segment. indices.append([v1, v4, v5]) indices.append([v2, v1, v5]) indices.append([v2, v5, v6]) indices.append([v3, v2, v6]) indices.append([v3, v6, v4]) indices.append([v1, v3, v4]) if color: #If we have a colour, add it to the vertices. colors.append([color.r, color.g, color.b, color.a]) colors.append([color.r, color.g, color.b, color.a]) colors.append([color.r, color.g, color.b, color.a]) #Rotate the resulting torus around the specified axis. matrix = Matrix() matrix.setByRotationAxis(angle, axis) vertices = numpy.asarray(vertices, dtype=numpy.float32) vertices = vertices.dot(matrix.getData()[0:3, 0:3]) vertices[:] += center.getData( ) #And translate to the desired position. self.addVertices(vertices) self.addIndices(numpy.asarray(indices, dtype=numpy.int32)) self.addColors(numpy.asarray(colors, dtype=numpy.float32))
def addCube(self, width, height, depth, center=Vector(0, 0, 0), color=None): """Add a rectangular cuboid to the mesh of this mesh builder. A rectangular cuboid is a square block with arbitrary width, height and depth. :param width: The size of the rectangular cuboid in the X dimension. :param height: The size of the rectangular cuboid in the Y dimension. :param depth: The size of the rectangular cuboid in the Z dimension. :param center: (Optional) The position of the centre of the rectangular cuboid in space. If not provided, the cuboid is placed at the coordinate origin. :param color: (Optional) The colour for the rectangular cuboid. If no colour is provided, the colour is determined by the shader. """ #Compute the actual positions of the planes. minW = -width / 2 + center.x maxW = width / 2 + center.x minH = -height / 2 + center.y maxH = height / 2 + center.y minD = -depth / 2 + center.z maxD = depth / 2 + center.z start = self.getVertexCount() verts = numpy.asarray( [ #All 8 corners. [minW, minH, maxD], [minW, maxH, maxD], [maxW, maxH, maxD], [maxW, minH, maxD], [minW, minH, minD], [minW, maxH, minD], [maxW, maxH, minD], [maxW, minH, minD], ], dtype=numpy.float32) self.addVertices(verts) indices = numpy.asarray( [ #All 6 quads (12 triangles). [start, start + 2, start + 1], [start, start + 3, start + 2], [start + 3, start + 7, start + 6], [start + 3, start + 6, start + 2], [start + 7, start + 5, start + 6], [start + 7, start + 4, start + 5], [start + 4, start + 1, start + 5], [start + 4, start + 0, start + 1], [start + 1, start + 6, start + 5], [start + 1, start + 2, start + 6], [start + 0, start + 7, start + 3], [start + 0, start + 4, start + 7] ], dtype=numpy.int32) self.addIndices(indices) if color: #If we have a colour, add a colour to all of the vertices. vertex_count = self.getVertexCount() for i in range(1, 9): self.setVertexColor(vertex_count - i, color)
def _generateSceneNode(self, file_name, xz_size, height_from_base, base_height, blur_iterations, max_size, lighter_is_higher, use_transparency_model, transmittance_1mm): 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) height_from_base = max(height_from_base, 0) base_height = max(base_height, 0) peak_height = base_height + height_from_base 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) if use_transparency_model: height_data[y, x] = ( 0.299 * math.pow(qRed(qrgb) / 255.0, 2.2) + 0.587 * math.pow(qGreen(qrgb) / 255.0, 2.2) + 0.114 * math.pow(qBlue(qrgb) / 255.0, 2.2)) else: height_data[y, x] = ( 0.212655 * qRed(qrgb) + 0.715158 * qGreen(qrgb) + 0.072187 * qBlue(qrgb) ) / 255 # fast computation ignoring gamma and degamma Job.yieldThread() if lighter_is_higher == use_transparency_model: 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() if use_transparency_model: divisor = 1.0 / math.log( transmittance_1mm / 100.0 ) # log-base doesn't matter here. Precompute this value for faster computation of each pixel. min_luminance = (transmittance_1mm / 100.0)**(peak_height - base_height) for (y, x) in numpy.ndindex(height_data.shape): mapped_luminance = min_luminance + ( 1.0 - min_luminance) * height_data[y, x] height_data[y, x] = base_height + divisor * math.log( mapped_luminance ) # use same base as a couple lines above this else: height_data *= scale_vector.y height_data += base_height if img.hasAlphaChannel(): for x in range(0, width): for y in range(0, height): height_data[y, x] *= qAlpha(img.pixel(x, y)) / 255.0 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 processGCodeFile(self, file_name): Logger.log("d", "Preparing to load %s" % file_name) self._cancelled = False # We obtain the filament diameter from the selected printer to calculate line widths self._filament_diameter = Application.getInstance( ).getGlobalContainerStack().getProperty("material_diameter", "value") 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 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, title=catalog.i18nc( "@info:title", "G-code Details")) self._message.setProgress(0) self._message.show() Logger.log("d", "Parsing %s..." % file_name) current_position = self._position(0, 0, 0, 0, [0]) current_path = [] min_layer_number = 0 negative_layers = 0 previous_layer = 0 for line in file: if self._cancelled: Logger.log("d", "Parsing %s cancelled" % file_name) return None current_line += 1 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) # When the layer change is reached, the polygon is computed so we have just one layer per layer per extruder 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() # When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior # as in ProcessSlicedLayersJob if layer_number < min_layer_number: min_layer_number = layer_number if layer_number < 0: layer_number += abs(min_layer_number) negative_layers += 1 else: layer_number += negative_layers # In case there is a gap in the layer count, empty layers are created for empty_layer in range(previous_layer + 1, layer_number): self._createEmptyLayer(empty_layer) self._layer_number = layer_number previous_layer = 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: # When find a movement, the new posistion is calculated and added to the current_path, but # don't need to create a polygon until the end of the layer current_position = self.processGCode( G, line, current_position, current_path) continue # When changing the extruder, the polygon with the stored paths is computed 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) if line.startswith("M"): M = self._getInt(line, "M") self.processMCode(M, line, current_position, current_path) # "Flush" leftovers. Last layer paths are still stored if 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, title=catalog.i18nc( "@info:title", "G-code Details")) 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 test_getSetMirror(self): node = SceneNode() node.setMirror(Vector(-1, 1, 1)) assert node.getMirror() == Vector(-1, 1, 1)
def read(self, file_name): result = SceneNode() # The base object of 3mf is a zipped archive. archive = zipfile.ZipFile(file_name, "r") try: root = ET.parse(archive.open("3D/3dmodel.model")) # There can be multiple objects, try to load all of them. objects = root.findall("./3mf:resources/3mf:object", self._namespaces) if len(objects) == 0: Logger.log("w", "No objects found in 3MF file %s, either the file is corrupt or you are using an outdated format", file_name) return None for entry in objects: mesh = MeshData() node = SceneNode() vertex_list = [] #for vertex in entry.mesh.vertices.vertex: for vertex in entry.findall(".//3mf:vertex", self._namespaces): vertex_list.append([vertex.get("x"), vertex.get("y"), vertex.get("z")]) Job.yieldThread() triangles = entry.findall(".//3mf:triangle", self._namespaces) mesh.reserveFaceCount(len(triangles)) #for triangle in object.mesh.triangles.triangle: for triangle in triangles: v1 = int(triangle.get("v1")) v2 = int(triangle.get("v2")) v3 = int(triangle.get("v3")) mesh.addFace(vertex_list[v1][0],vertex_list[v1][1],vertex_list[v1][2],vertex_list[v2][0],vertex_list[v2][1],vertex_list[v2][2],vertex_list[v3][0],vertex_list[v3][1],vertex_list[v3][2]) Job.yieldThread() # Rotate the model; We use a different coordinate frame. rotation = Matrix() rotation.setByRotationAxis(-0.5 * math.pi, Vector(1,0,0)) mesh = mesh.getTransformed(rotation) #TODO: We currently do not check for normals and simply recalculate them. mesh.calculateNormals() node.setMeshData(mesh) node.setSelectable(True) transformation = root.findall("./3mf:build/3mf:item[@objectid='{0}']".format(entry.get("id")), self._namespaces) if transformation: transformation = transformation[0] try: if transformation.get("transform"): splitted_transformation = transformation.get("transform").split() ## Transformation is saved as: ## M00 M01 M02 0.0 ## M10 M11 M12 0.0 ## M20 M21 M22 0.0 ## M30 M31 M32 1.0 ## We switch the row & cols as that is how everyone else uses matrices! temp_mat = Matrix() # Rotation & Scale temp_mat._data[0,0] = splitted_transformation[0] temp_mat._data[1,0] = splitted_transformation[1] temp_mat._data[2,0] = splitted_transformation[2] temp_mat._data[0,1] = splitted_transformation[3] temp_mat._data[1,1] = splitted_transformation[4] temp_mat._data[2,1] = splitted_transformation[5] temp_mat._data[0,2] = splitted_transformation[6] temp_mat._data[1,2] = splitted_transformation[7] temp_mat._data[2,2] = splitted_transformation[8] # Translation temp_mat._data[0,3] = splitted_transformation[9] temp_mat._data[1,3] = splitted_transformation[10] temp_mat._data[2,3] = splitted_transformation[11] node.setTransformation(temp_mat) except AttributeError: pass # Empty list was found. Getting transformation is not possible result.addChild(node) Job.yieldThread() #If there is more then one object, group them. try: if len(objects) > 1: group_decorator = GroupDecorator() result.addDecorator(group_decorator) except: pass except Exception as e: Logger.log("e" ,"exception occured in 3mf reader: %s" , e) return result
def _getNullBoundingBox(): return AxisAlignedBox(minimum=Vector(0, 0, 0), maximum=Vector(10, 10, 10))
def _createCustom(self, size, maxs, pos1 , pos2, dep, ztop): mesh = MeshBuilder() # Init point Pt1 = Vector(pos1.x,pos1.z,pos1.y) Pt2 = Vector(pos2.x,pos2.z,pos2.y) V_Dir = Pt2 - Pt1 # Calcul vecteur s = size / 2 sm = maxs / 2 l_a = pos1.y s_infa=math.tan(math.radians(dep))*l_a+s l_b = pos2.y s_infb=math.tan(math.radians(dep))*l_b+s if sm>s and dep!=0: l_max_a=(sm-s) / math.tan(math.radians(dep)) l_max_b=(sm-s) / math.tan(math.radians(dep)) else : l_max_a=l_a l_max_b=l_b Vtop = Vector(0,0,ztop) VZ = Vector(0,0,s) VZa = Vector(0,0,-l_a) VZb = Vector(0,0,-l_b) Norm=Vector.cross(V_Dir,VZ).normalized() Dec = Vector(Norm.x*s,Norm.y*s,Norm.z*s) if l_max_a<l_a and l_max_b<l_b and l_max_a>0 and l_max_b>0: nbv=40 Deca = Vector(Norm.x*sm,Norm.y*sm,Norm.z*sm) Decb = Vector(Norm.x*sm,Norm.y*sm,Norm.z*sm) VZam = Vector(0,0,-l_max_a) VZbm = Vector(0,0,-l_max_b) # X Z Y P_1t = Vtop+Dec P_2t = Vtop-Dec P_3t = V_Dir+Vtop+Dec P_4t = V_Dir+Vtop-Dec P_1m = VZam+Deca P_2m = VZam-Deca P_3m = VZbm+V_Dir+Decb P_4m = VZbm+V_Dir-Decb P_1i = VZa+Deca P_2i = VZa-Deca P_3i = VZb+V_Dir+Decb P_4i = VZb+V_Dir-Decb """ 1) Top 2) Front 3) Left 4) Right 5) Back 6) Front inf 7) Left inf 8) Right inf 9) Back inf 10) Bottom """ verts = [ # 10 faces with 4 corners each [P_1t.x, P_1t.z, P_1t.y], [P_2t.x, P_2t.z, P_2t.y], [P_4t.x, P_4t.z, P_4t.y], [P_3t.x, P_3t.z, P_3t.y], [P_1t.x, P_1t.z, P_1t.y], [P_3t.x, P_3t.z, P_3t.y], [P_3m.x, P_3m.z, P_3m.y], [P_1m.x, P_1m.z, P_1m.y], [P_2t.x, P_2t.z, P_2t.y], [P_1t.x, P_1t.z, P_1t.y], [P_1m.x, P_1m.z, P_1m.y], [P_2m.x, P_2m.z, P_2m.y], [P_3t.x, P_3t.z, P_3t.y], [P_4t.x, P_4t.z, P_4t.y], [P_4m.x, P_4m.z, P_4m.y], [P_3m.x, P_3m.z, P_3m.y], [P_4t.x, P_4t.z, P_4t.y], [P_2t.x, P_2t.z, P_2t.y], [P_2m.x, P_2m.z, P_2m.y], [P_4m.x, P_4m.z, P_4m.y], [P_1m.x, P_1m.z, P_1m.y], [P_3m.x, P_3m.z, P_3m.y], [P_3i.x, P_3i.z, P_3i.y], [P_1i.x, P_1i.z, P_1i.y], [P_2m.x, P_2m.z, P_2m.y], [P_1m.x, P_1m.z, P_1m.y], [P_1i.x, P_1i.z, P_1i.y], [P_2i.x, P_2i.z, P_2i.y], [P_3m.x, P_3m.z, P_3m.y], [P_4m.x, P_4m.z, P_4m.y], [P_4i.x, P_4i.z, P_4i.y], [P_3i.x, P_3i.z, P_3i.y], [P_4m.x, P_4m.z, P_4m.y], [P_2m.x, P_2m.z, P_2m.y], [P_2i.x, P_2i.z, P_2i.y], [P_4i.x, P_4i.z, P_4i.y], [P_1i.x, P_1i.z, P_1i.y], [P_2i.x, P_2i.z, P_2i.y], [P_4i.x, P_4i.z, P_4i.y], [P_3i.x, P_3i.z, P_3i.y] ] else: nbv=24 Deca = Vector(Norm.x*s_infa,Norm.y*s_infa,Norm.z*s_infa) Decb = Vector(Norm.x*s_infb,Norm.y*s_infb,Norm.z*s_infb) # X Z Y P_1t = Vtop+Dec P_2t = Vtop-Dec P_3t = V_Dir+Vtop+Dec P_4t = V_Dir+Vtop-Dec P_1i = VZa+Deca P_2i = VZa-Deca P_3i = VZb+V_Dir+Decb P_4i = VZb+V_Dir-Decb """ 1) Top 2) Front 3) Left 4) Right 5) Back 6) Bottom """ verts = [ # 6 faces with 4 corners each [P_1t.x, P_1t.z, P_1t.y], [P_2t.x, P_2t.z, P_2t.y], [P_4t.x, P_4t.z, P_4t.y], [P_3t.x, P_3t.z, P_3t.y], [P_1t.x, P_1t.z, P_1t.y], [P_3t.x, P_3t.z, P_3t.y], [P_3i.x, P_3i.z, P_3i.y], [P_1i.x, P_1i.z, P_1i.y], [P_2t.x, P_2t.z, P_2t.y], [P_1t.x, P_1t.z, P_1t.y], [P_1i.x, P_1i.z, P_1i.y], [P_2i.x, P_2i.z, P_2i.y], [P_3t.x, P_3t.z, P_3t.y], [P_4t.x, P_4t.z, P_4t.y], [P_4i.x, P_4i.z, P_4i.y], [P_3i.x, P_3i.z, P_3i.y], [P_4t.x, P_4t.z, P_4t.y], [P_2t.x, P_2t.z, P_2t.y], [P_2i.x, P_2i.z, P_2i.y], [P_4i.x, P_4i.z, P_4i.y], [P_1i.x, P_1i.z, P_1i.y], [P_2i.x, P_2i.z, P_2i.y], [P_4i.x, P_4i.z, P_4i.y], [P_3i.x, P_3i.z, P_3i.y] ] mesh.setVertices(numpy.asarray(verts, dtype=numpy.float32)) indices = [] for i in range(0, nbv, 4): # All 6 quads (12 triangles) indices.append([i, i+2, i+1]) indices.append([i, i+3, i+2]) mesh.setIndices(numpy.asarray(indices, dtype=numpy.int32)) mesh.calculateNormals() return mesh
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 isinstance(node, SceneNode): continue 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: if not cls.__selection: cls.__selection_center = Vector.Null 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 event(self, event): super().event(event) 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: if event.key == KeyEvent.ShiftKey: self._lock_steps = False self.propertyChanged.emit() elif event.key == KeyEvent.ControlKey: self._non_uniform_scale = True self.propertyChanged.emit() if event.type == Event.KeyReleaseEvent: if event.key == KeyEvent.ShiftKey: self._lock_steps = True self.propertyChanged.emit() elif event.key == KeyEvent.ControlKey: self._non_uniform_scale = False self.propertyChanged.emit() if event.type == Event.MousePressEvent: if MouseEvent.LeftButton not in event.buttons: return False id = self._renderer.getIdAtCoordinate(event.x, event.y) if not id: return False if ToolHandle.isAxis(id): self.setLockedAxis(id) handle_position = self._handle.getWorldPosition() if id == ToolHandle.XAxis: self.setDragPlane(Plane(Vector(0, 0, 1), handle_position.z)) elif id == ToolHandle.YAxis: self.setDragPlane(Plane(Vector(0, 0, 1), handle_position.z)) elif id == ToolHandle.ZAxis: self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y)) else: self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y)) self.setDragStart(event.x, event.y) self.operationStarted.emit(self) if event.type == Event.MouseMoveEvent: if not self.getDragPlane(): return False handle_position = self._handle.getWorldPosition() drag_position = self.getDragPosition(event.x, event.y) if drag_position: drag_length = (drag_position - handle_position).length() if self._drag_length > 0: drag_change = (drag_length - self._drag_length) / 100 if self._snap_scale and abs(drag_change) < self._snap_amount: return False scale = Vector(1.0, 1.0, 1.0) if self._non_uniform_scale: if self.getLockedAxis() == ToolHandle.XAxis: scale.setX(1.0 + drag_change) elif self.getLockedAxis() == ToolHandle.YAxis: scale.setY(1.0 + drag_change) elif self.getLockedAxis() == ToolHandle.ZAxis: scale.setZ(1.0 + drag_change) if scale == Vector(1.0, 1.0, 1.0): scale.setX(1.0 + drag_change) scale.setY(1.0 + drag_change) scale.setZ(1.0 + drag_change) Selection.applyOperation(ScaleOperation, scale) self._drag_length = (handle_position - drag_position).length() return True if event.type == Event.MouseReleaseEvent: if self.getDragPlane(): self.setDragPlane(None) self.setLockedAxis(None) self._drag_length = 0 self.operationStopped.emit(self) return True
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 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 # Use extruder 0 if the extruder manager reports extruder index -1 (for single extrusion printers) self._layer_shader.setUniformValue("u_active_extruder", float(max(0, self._extruder_manager.activeExtruderIndex))) if self._layer_view: self._layer_shader.setUniformValue("u_max_feedrate", self._layer_view.getMaxFeedrate()) self._layer_shader.setUniformValue("u_min_feedrate", self._layer_view.getMinFeedrate()) self._layer_shader.setUniformValue("u_max_thickness", self._layer_view.getMaxThickness()) self._layer_shader.setUniformValue("u_min_thickness", self._layer_view.getMinThickness()) self._layer_shader.setUniformValue("u_layer_view_type", self._layer_view.getSimulationViewType()) self._layer_shader.setUniformValue("u_extruder_opacity", self._layer_view.getExtruderOpacities()) self._layer_shader.setUniformValue("u_show_travel_moves", self._layer_view.getShowTravelMoves()) self._layer_shader.setUniformValue("u_show_helpers", self._layer_view.getShowHelpers()) self._layer_shader.setUniformValue("u_show_skin", self._layer_view.getShowSkin()) self._layer_shader.setUniformValue("u_show_infill", self._layer_view.getShowInfill()) else: #defaults self._layer_shader.setUniformValue("u_max_feedrate", 1) self._layer_shader.setUniformValue("u_min_feedrate", 0) self._layer_shader.setUniformValue("u_max_thickness", 1) self._layer_shader.setUniformValue("u_min_thickness", 0) self._layer_shader.setUniformValue("u_layer_view_type", 1) self._layer_shader.setUniformValue("u_extruder_opacity", [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]) self._layer_shader.setUniformValue("u_show_travel_moves", 0) self._layer_shader.setUniformValue("u_show_helpers", 1) self._layer_shader.setUniformValue("u_show_skin", 1) self._layer_shader.setUniformValue("u_show_infill", 1) 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())) if not self._disabled_shader: self._disabled_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "striped.shader")) self._disabled_shader.setUniformValue("u_diffuseColor1", Color(*Application.getInstance().getTheme().getColor("model_unslicable").getRgb())) self._disabled_shader.setUniformValue("u_diffuseColor2", Color(*Application.getInstance().getTheme().getColor("model_unslicable_alt").getRgb())) self._disabled_shader.setUniformValue("u_width", 50.0) self._disabled_shader.setUniformValue("u_opacity", 0.6) self.bind() tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Overlay, backface_cull = True) disabled_batch = RenderBatch(self._disabled_shader) 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) # Don't set to true, we render it separately! elif getattr(node, "_outside_buildarea", False) and isinstance(node, SceneNode) and node.getMeshData() and node.isVisible() and not node.callDecoration("isNonPrintingMesh"): disabled_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData()) 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 = 0 end = 0 element_counts = layer_data.getElementCounts() for layer in sorted(element_counts.keys()): # In the current layer, we show just the indicated paths if layer == self._layer_view._current_layer_num: # We look for the position of the head, searching the point of the current path index = self._layer_view._current_path_num offset = 0 for polygon in layer_data.getLayer(layer).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 break if self._layer_view._minimum_layer_num > layer: start += element_counts[layer] end += element_counts[layer] # 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.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()) if len(disabled_batch.items) > 0: disabled_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 run(self): self.showSplashMessage( self._i18n_catalog.i18nc("@info:progress", "Setting up scene...")) controller = self.getController() controller.setActiveView("SolidView") controller.setCameraTool("CameraTool") controller.setSelectionTool("SelectionTool") t = controller.getTool("TranslateTool") if t: t.setEnabledAxis( [ToolHandle.XAxis, ToolHandle.YAxis, ToolHandle.ZAxis]) Selection.selectionChanged.connect(self.onSelectionChanged) root = controller.getScene().getRoot() # The platform is a child of BuildVolume self._volume = BuildVolume.BuildVolume(root) self.getRenderer().setBackgroundColor(QColor(245, 245, 245)) self._physics = PlatformPhysics.PlatformPhysics( controller, self._volume) camera = Camera("3d", root) camera.setPosition(Vector(-80, 250, 700)) camera.setPerspective(True) camera.lookAt(Vector(0, 0, 0)) controller.getScene().setActiveCamera("3d") self.getController().getTool("CameraTool").setOrigin(Vector(0, 100, 0)) self._camera_animation = CameraAnimation.CameraAnimation() self._camera_animation.setCameraTool( self.getController().getTool("CameraTool")) self.showSplashMessage( self._i18n_catalog.i18nc("@info:progress", "Loading interface...")) # Initialise extruder so as to listen to global container stack changes before the first global container stack is set. cura.Settings.ExtruderManager.getInstance() qmlRegisterSingletonType(cura.Settings.MachineManager, "Cura", 1, 0, "MachineManager", self.getMachineManager) qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager) self.setMainQml( Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml")) self._qml_import_paths.append( Resources.getPath(self.ResourceTypes.QmlFiles)) self.initializeEngine() if self._engine.rootObjects: self.closeSplash() for file in self.getCommandLineOption("file", []): self._openFile(file) for file_name in self._open_file_queue: #Open all the files that were queued up while plug-ins were loading. self._openFile(file_name) self._started = True self.exec_()
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 node.getBoundingBox() is None: continue bbox = node.getBoundingBox() # Move it downwards if bottom is above platform move_vector = Vector() if Application.getInstance().getPreferences().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root) 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) and not node.callDecoration("isNonPrintingMesh"): node.addDecorator(ConvexHullDecorator()) # only push away objects if this node is a printing mesh if not node.callDecoration("isNonPrintingMesh") and Application.getInstance().getPreferences().getValue("physics/automatic_push_free"): # Do not move locked nodes if node.getSetting(SceneNodeSettings.LockPosition): continue # 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 _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 event(self, event): super().event(event) if event.type == Event.ToolActivateEvent: self._old_scale = Selection.getSelectedObject(0).getScale() 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) # Handle modifier keys: Shift toggles snap, Control toggles uniform scaling if event.type == Event.KeyPressEvent: if event.key == KeyEvent.ShiftKey: self._snap_scale = False self.propertyChanged.emit() elif event.key == KeyEvent.ControlKey: self._non_uniform_scale = True self.propertyChanged.emit() if event.type == Event.KeyReleaseEvent: if event.key == KeyEvent.ShiftKey: self._snap_scale = True self.propertyChanged.emit() elif event.key == KeyEvent.ControlKey: self._non_uniform_scale = False self.propertyChanged.emit() 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 ToolHandle.isAxis(id): self.setLockedAxis(id) # Save the current positions of the node, as we want to scale arround their current centres self._saved_node_positions = [] for node in Selection.getAllSelectedObjects(): self._saved_node_positions.append((node, node.getWorldPosition())) self._saved_handle_position = self._handle.getWorldPosition() 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) self.operationStarted.emit(self) 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: 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._snap_scale: scale_factor = round(drag_change, 1) else: scale_factor = drag_change 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: scale_change = Vector(x=scale_factor, y=scale_factor, z=scale_factor) # Scale around the saved centeres of all selected nodes 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() self._drag_length = (self._saved_handle_position - drag_position).length() else: self._drag_length = (self._saved_handle_position - drag_position).length() #First move, do nothing but set right length. return True if event.type == Event.MouseReleaseEvent: # Finish a scale operation if self.getDragPlane(): self.setDragPlane(None) self.setLockedAxis(None) self._drag_length = 0 self.operationStopped.emit(self) return True
def _onChangeTimerFinished(self): if not self._enabled: return root = self._controller.getScene().getRoot() 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().set(bottom=-9001) 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 not (node.getParent() and node.getParent().callDecoration("isGroup")): #If an object is grouped, don't move it down z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0 if bbox.bottom > 0: move_vector = move_vector.set(y=-bbox.bottom + z_offset) elif bbox.bottom < z_offset: 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()) node.callDecoration("recomputeConvexHull") if Preferences.getInstance().getValue("physics/automatic_push_free"): # Check for collisions between convex hulls for other_node in BreadthFirstIterator(root): # Ignore root, ourselves and anything that is not a normal SceneNode. 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 # 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("getConvexHull")) if not overlap: other_head_hull = other_node.callDecoration("getConvexHullHead") if other_head_hull: overlap = node.callDecoration("getConvexHull").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): op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector) op.push()
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode): self._archive = None # Reset archive archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED) try: model_file = zipfile.ZipInfo("3D/3dmodel.model") # Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo. model_file.compress_type = zipfile.ZIP_DEFLATED # Create content types file content_types_file = zipfile.ZipInfo("[Content_Types].xml") content_types_file.compress_type = zipfile.ZIP_DEFLATED content_types = ET.Element("Types", xmlns = self._namespaces["content-types"]) rels_type = ET.SubElement(content_types, "Default", Extension = "rels", ContentType = "application/vnd.openxmlformats-package.relationships+xml") model_type = ET.SubElement(content_types, "Default", Extension = "model", ContentType = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml") # Create _rels/.rels file relations_file = zipfile.ZipInfo("_rels/.rels") relations_file.compress_type = zipfile.ZIP_DEFLATED relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"]) model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel") # Attempt to add a thumbnail snapshot = self._createSnapshot() if snapshot: thumbnail_buffer = QBuffer() thumbnail_buffer.open(QBuffer.ReadWrite) snapshot.save(thumbnail_buffer, "PNG") thumbnail_file = zipfile.ZipInfo("Metadata/thumbnail.png") # Don't try to compress snapshot file, because the PNG is pretty much as compact as it will get archive.writestr(thumbnail_file, thumbnail_buffer.data()) # Add PNG to content types file thumbnail_type = ET.SubElement(content_types, "Default", Extension = "png", ContentType = "image/png") # Add thumbnail relation to _rels/.rels file thumbnail_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/Metadata/thumbnail.png", Id = "rel1", Type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail") savitar_scene = Savitar.Scene() metadata_to_store = CuraApplication.getInstance().getController().getScene().getMetaData() for key, value in metadata_to_store.items(): savitar_scene.setMetaDataEntry(key, value) current_time_string = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") if "Application" not in metadata_to_store: # This might sound a bit strange, but this field should store the original application that created # the 3mf. So if it was already set, leave it to whatever it was. savitar_scene.setMetaDataEntry("Application", CuraApplication.getInstance().getApplicationDisplayName()) if "CreationDate" not in metadata_to_store: savitar_scene.setMetaDataEntry("CreationDate", current_time_string) savitar_scene.setMetaDataEntry("ModificationDate", current_time_string) 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 global_container_stack = Application.getInstance().getGlobalContainerStack() # 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.preMultiply(translation_matrix) root_node = UM.Application.Application.getInstance().getController().getScene().getRoot() for node in nodes: if node == root_node: for root_child in node.getChildren(): savitar_node = self._convertUMNodeToSavitarNode(root_child, transformation_matrix) if savitar_node: savitar_scene.addSceneNode(savitar_node) else: savitar_node = self._convertUMNodeToSavitarNode(node, transformation_matrix) if savitar_node: savitar_scene.addSceneNode(savitar_node) parser = Savitar.ThreeMFParser() scene_string = parser.sceneToString(savitar_scene) archive.writestr(model_file, scene_string) archive.writestr(content_types_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types)) archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element)) except Exception as e: Logger.logException("e", "Error writing zip file") self.setInformation(catalog.i18nc("@error:zip", "Error writing 3mf file.")) return False finally: if not self._store_archive: archive.close() else: self._archive = archive return True
def setByTranslation(self, direction: Vector): M = numpy.identity(4, dtype=numpy.float64) M[:3, 3] = direction.getData()[:3] self._data = M
class SceneNode(SignalEmitter): class TransformSpace: Local = 1 Parent = 2 World = 3 def __init__(self, parent = None, name = ""): super().__init__() # Call super to make multiple inheritence work. self._children = [] self._mesh_data = None self._position = Vector() self._scale = Vector(1.0, 1.0, 1.0) self._mirror = Vector(1.0, 1.0, 1.0) self._orientation = Quaternion() self._transformation = None self._world_transformation = None self._derived_position = None self._derived_orientation = None self._derived_scale = None self._inherit_orientation = True self._inherit_scale = True self._parent = parent self._enabled = True self._selectable = False self._calculate_aabb = True self._aabb = None self._aabb_job = None self._visible = True self._name = name self._decorators = [] self._bounding_box_mesh = None self.boundingBoxChanged.connect(self.calculateBoundingBoxMesh) self.parentChanged.connect(self._onParentChanged) if parent: parent.addChild(self) def __deepcopy__(self, memo): copy = SceneNode() copy.translate(self.getPosition()) copy.setOrientation(self.getOrientation()) copy.setScale(self.getScale()) copy.setMeshData(deepcopy(self._mesh_data, memo)) copy.setVisible(deepcopy(self._visible, memo)) copy._selectable = deepcopy(self._selectable, memo) for decorator in self._decorators: copy.addDecorator(deepcopy(decorator, memo)) for child in self._children: copy.addChild(deepcopy(child, memo)) self.calculateBoundingBoxMesh() return copy def setCenterPosition(self, center): if self._mesh_data: m = Matrix() m.setByTranslation(-center) self._mesh_data = self._mesh_data.getTransformed(m) self._mesh_data.setCenterPosition(center) for child in self._children: child.setCenterPosition(center) ## \brief Get the parent of this node. If the node has no parent, it is the root node. # \returns SceneNode if it has a parent and None if it's the root node. def getParent(self): return self._parent def getBoundingBoxMesh(self): return self._bounding_box_mesh def calculateBoundingBoxMesh(self): if self._aabb: self._bounding_box_mesh = MeshData() rtf = self._aabb.maximum lbb = self._aabb.minimum self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back else: self._resetAABB() def _onParentChanged(self, node): for child in self.getChildren(): child.parentChanged.emit(self) decoratorsChanged = Signal() def addDecorator(self, decorator): decorator.setNode(self) self._decorators.append(decorator) self.decoratorsChanged.emit(self) def getDecorators(self): return self._decorators def getDecorator(self, dec_type): for decorator in self._decorators: if type(decorator) == dec_type: return decorator def removeDecorators(self): self._decorators = [] self.decoratorsChanged.emit(self) def removeDecorator(self, dec_type): for decorator in self._decorators: if type(decorator) == dec_type: self._decorators.remove(decorator) self.decoratorsChanged.emit(self) break def callDecoration(self, function, *args, **kwargs): for decorator in self._decorators: if hasattr(decorator, function): try: return getattr(decorator, function)(*args, **kwargs) except Exception as e: Logger.log("e", "Exception calling decoration %s: %s", str(function), str(e)) return None def hasDecoration(self, function): for decorator in self._decorators: if hasattr(decorator, function): return True return False def getName(self): return self._name def setName(self, name): self._name = name ## How many nodes is this node removed from the root def getDepth(self): if self._parent is None: return 0 return self._parent.getDepth() + 1 ## \brief Set the parent of this object # \param scene_node SceneNode that is the parent of this object. def setParent(self, scene_node): if self._parent: self._parent.removeChild(self) #self._parent = scene_node if scene_node: scene_node.addChild(self) ## Emitted whenever the parent changes. parentChanged = Signal() ## \brief Get the visibility of this node. The parents visibility overrides the visibility. # TODO: Let renderer actually use the visibility to decide wether to render or not. def isVisible(self): if self._parent != None and self._visible: return self._parent.isVisible() else: return self._visible def setVisible(self, visible): self._visible = visible ## \brief Get the (original) mesh data from the scene node/object. # \returns MeshData def getMeshData(self): return self._mesh_data ## \brief Get the transformed mesh data from the scene node/object, based on the transformation of scene nodes wrt root. # \returns MeshData def getMeshDataTransformed(self): #transformed_mesh = deepcopy(self._mesh_data) #transformed_mesh.transform(self.getWorldTransformation()) return self._mesh_data.getTransformed(self.getWorldTransformation()) ## \brief Set the mesh of this node/object # \param mesh_data MeshData object def setMeshData(self, mesh_data): if self._mesh_data: self._mesh_data.dataChanged.disconnect(self._onMeshDataChanged) self._mesh_data = mesh_data if self._mesh_data is not None: self._mesh_data.dataChanged.connect(self._onMeshDataChanged) self._resetAABB() self.meshDataChanged.emit(self) ## Emitted whenever the attached mesh data object changes. meshDataChanged = Signal() def _onMeshDataChanged(self): self.meshDataChanged.emit(self) ## \brief Add a child to this node and set it's parent as this node. # \params scene_node SceneNode to add. def addChild(self, scene_node): if scene_node not in self._children: scene_node.transformationChanged.connect(self.transformationChanged) scene_node.childrenChanged.connect(self.childrenChanged) scene_node.meshDataChanged.connect(self.meshDataChanged) self._children.append(scene_node) self._resetAABB() self.childrenChanged.emit(self) if not scene_node._parent is self: scene_node._parent = self scene_node._transformChanged() scene_node.parentChanged.emit(self) ## \brief remove a single child # \param child Scene node that needs to be removed. def removeChild(self, child): if child not in self._children: return child.transformationChanged.disconnect(self.transformationChanged) child.childrenChanged.disconnect(self.childrenChanged) child.meshDataChanged.disconnect(self.meshDataChanged) self._children.remove(child) child._parent = None child._transformChanged() child.parentChanged.emit(self) self.childrenChanged.emit(self) ## \brief Removes all children and its children's children. def removeAllChildren(self): for child in self._children: child.removeAllChildren() self.removeChild(child) self.childrenChanged.emit(self) ## \brief Get the list of direct children # \returns List of children def getChildren(self): return self._children def hasChildren(self): return True if self._children else False ## \brief Get list of all children (including it's children children children etc.) # \returns list ALl children in this 'tree' def getAllChildren(self): children = [] children.extend(self._children) for child in self._children: children.extend(child.getAllChildren()) return children ## \brief Emitted whenever the list of children of this object or any child object changes. # \param object The object that triggered the change. childrenChanged = Signal() ## \brief Computes and returns the transformation from world to local space. # \returns 4x4 transformation matrix def getWorldTransformation(self): if self._world_transformation is None: self._updateTransformation() return deepcopy(self._world_transformation) ## \brief Returns the local transformation with respect to its parent. (from parent to local) # \retuns transformation 4x4 (homogenous) matrix def getLocalTransformation(self): if self._transformation is None: self._updateTransformation() return deepcopy(self._transformation) ## Get the local orientation value. def getOrientation(self): return deepcopy(self._orientation) ## \brief Rotate the scene object (and thus its children) by given amount # # \param rotation \type{Quaternion} A quaternion indicating the amount of rotation. # \param transform_space The space relative to which to rotate. Can be any one of the constants in SceneNode::TransformSpace. def rotate(self, rotation, transform_space = TransformSpace.Local): if not self._enabled: return if transform_space == SceneNode.TransformSpace.Local: self._orientation = self._orientation * rotation elif transform_space == SceneNode.TransformSpace.Parent: self._orientation = rotation * self._orientation elif transform_space == SceneNode.TransformSpace.World: self._orientation = self._orientation * self._getDerivedOrientation().getInverse() * rotation * self._getDerivedOrientation() else: raise ValueError("Unknown transform space {0}".format(transform_space)) self._orientation.normalize() self._transformChanged() ## Set the local orientation of this scene node. # # \param orientation \type{Quaternion} The new orientation of this scene node. def setOrientation(self, orientation): if not self._enabled or orientation == self._orientation: return self._orientation = orientation self._orientation.normalize() self._transformChanged() ## Get the local scaling value. def getScale(self): return deepcopy(self._scale) ## Scale the scene object (and thus its children) by given amount # # \param scale \type{Vector} A Vector with three scale values # \param transform_space The space relative to which to scale. Can be any one of the constants in SceneNode::TransformSpace. def scale(self, scale, transform_space = TransformSpace.Local): if not self._enabled: return if transform_space == SceneNode.TransformSpace.Local: self._scale = self._scale.scale(scale) elif transform_space == SceneNode.TransformSpace.Parent: raise NotImplementedError() elif transform_space == SceneNode.TransformSpace.World: if self._parent: scale_change = Vector(1,1,1) - scale if scale_change.x < 0 or scale_change.y < 0 or scale_change.z < 0: direction = -1 else: direction = 1 # Hackish way to do this, but this seems to correctly scale the object. change_vector = self._scale.scale(self._getDerivedOrientation().getInverse().rotate(scale_change)) if change_vector.x < 0 and direction == 1: change_vector.setX(-change_vector.x) if change_vector.x > 0 and direction == -1: change_vector.setX(-change_vector.x) if change_vector.y < 0 and direction == 1: change_vector.setY(-change_vector.y) if change_vector.y > 0 and direction == -1: change_vector.setY(-change_vector.y) if change_vector.z < 0 and direction == 1: change_vector.setZ(-change_vector.z) if change_vector.z > 0 and direction == -1: change_vector.setZ(-change_vector.z) self._scale -= self._scale.scale(change_vector) else: raise ValueError("Unknown transform space {0}".format(transform_space)) self._transformChanged() ## Set the local scale value. # # \param scale \type{Vector} The new scale value of the scene node. def setScale(self, scale): if not self._enabled or scale == self._scale: return self._scale = scale self._transformChanged() ## Get the local mirror values. # # \return The mirror values as vector of scaling values. def getMirror(self): return deepcopy(self._mirror) ## Mirror the scene object (and thus its children) in the given directions. # # \param mirror \type{Vector} A vector of three scale values that is used # to mirror the node. # \param transform_space The space relative to which to scale. Can be any # one of the constants in SceneNode::TransformSpace. def mirror(self, mirror, transform_space = TransformSpace.Local): if not self._enabled: return if transform_space == SceneNode.TransformSpace.Local: self._mirror *= mirror elif transform_space == SceneNode.TransformSpace.Parent: self._mirror *= mirror elif transform_space == SceneNode.TransformSpace.World: self._mirror *= mirror else: raise ValueError("Unknown transform space {0}".format(transform_space)) self._transformChanged() ## Set the local mirror values. # # \param mirror \type{Vector} The new mirror values as scale multipliers. def setMirror(self, mirror): if not self._enabled or mirror == self._mirror: return self._mirror = mirror self._transformChanged() ## Get the local position. def getPosition(self): return deepcopy(self._position) ## Get the position of this scene node relative to the world. def getWorldPosition(self): if not self._derived_position: self._updateTransformation() return deepcopy(self._derived_position) ## Translate the scene object (and thus its children) by given amount. # # \param translation \type{Vector} The amount to translate by. # \param transform_space The space relative to which to translate. Can be any one of the constants in SceneNode::TransformSpace. def translate(self, translation, transform_space = TransformSpace.Local): if not self._enabled: return if transform_space == SceneNode.TransformSpace.Local: self._position += self._orientation.rotate(translation) elif transform_space == SceneNode.TransformSpace.Parent: self._position += translation elif transform_space == SceneNode.TransformSpace.World: if self._parent: self._position += (1.0 / self._parent._getDerivedScale()).scale(self._parent._getDerivedOrientation().getInverse().rotate(translation)) else: self._position += translation self._transformChanged() ## Set the local position value. # # \param position The new position value of the SceneNode. def setPosition(self, position): if not self._enabled or position == self._position: return self._position = position self._transformChanged() ## Signal. Emitted whenever the transformation of this object or any child object changes. # \param object The object that caused the change. transformationChanged = Signal() ## Rotate this scene node in such a way that it is looking at target. # # \param target \type{Vector} The target to look at. # \param up \type{Vector} The vector to consider up. Defaults to Vector.Unit_Y, i.e. (0, 1, 0). def lookAt(self, target, up = Vector.Unit_Y): if not self._enabled: return eye = self.getWorldPosition() f = (target - eye).normalize() up.normalize() s = f.cross(up).normalize() u = s.cross(f).normalize() m = Matrix([ [ s.x, u.x, -f.x, 0.0], [ s.y, u.y, -f.y, 0.0], [ s.z, u.z, -f.z, 0.0], [ 0.0, 0.0, 0.0, 1.0] ]) if self._parent: self._orientation = self._parent._getDerivedOrientation() * Quaternion.fromMatrix(m) else: self._orientation = Quaternion.fromMatrix(m) self._transformChanged() ## Can be overridden by child nodes if they need to perform special rendering. # If you need to handle rendering in a special way, for example for tool handles, # you can override this method and render the node. Return True to prevent the # view from rendering any attached mesh data. # # \param renderer The renderer object to use for rendering. # # \return False if the view should render this node, True if we handle our own rendering. def render(self, renderer): return False ## Get whether this SceneNode is enabled, that is, it can be modified in any way. def isEnabled(self): if self._parent != None and self._enabled: return self._parent.isEnabled() else: return self._enabled ## Set whether this SceneNode is enabled. # \param enable True if this object should be enabled, False if not. # \sa isEnabled def setEnabled(self, enable): self._enabled = enable ## Get whether this SceneNode can be selected. # # \note This will return false if isEnabled() returns false. def isSelectable(self): return self._enabled and self._selectable ## Set whether this SceneNode can be selected. # # \param select True if this SceneNode should be selectable, False if not. def setSelectable(self, select): self._selectable = select ## Get the bounding box of this node and its children. # # Note that the AABB is calculated in a separate thread. This method will return an invalid (size 0) AABB # while the calculation happens. def getBoundingBox(self): if self._aabb: return self._aabb if not self._aabb_job: self._resetAABB() return AxisAlignedBox() ## Set whether or not to calculate the bounding box for this node. # # \param calculate True if the bounding box should be calculated, False if not. def setCalculateBoundingBox(self, calculate): self._calculate_aabb = calculate boundingBoxChanged = Signal() ## private: def _getDerivedPosition(self): if not self._derived_position: self._updateTransformation() return self._derived_position def _getDerivedOrientation(self): if not self._derived_orientation: self._updateTransformation() # Sometimes the derived orientation can be None. # I've not been able to locate the cause of this, but this prevents it being an issue. if not self._derived_orientation: self._derived_orientation = Quaternion() return self._derived_orientation def _getDerivedScale(self): if not self._derived_scale: self._updateTransformation() return self._derived_scale def _transformChanged(self): self._resetAABB() self._transformation = None self._world_transformation = None self._derived_position = None self._derived_orientation = None self._derived_scale = None self.transformationChanged.emit(self) for child in self._children: child._transformChanged() def _updateTransformation(self): scale_and_mirror = self._scale * self._mirror self._transformation = Matrix.fromPositionOrientationScale(self._position, self._orientation, scale_and_mirror) if self._parent: parent_orientation = self._parent._getDerivedOrientation() if self._inherit_orientation: self._derived_orientation = parent_orientation * self._orientation else: self._derived_orientation = self._orientation # Sometimes the derived orientation can be None. # I've not been able to locate the cause of this, but this prevents it being an issue. if not self._derived_orientation: self._derived_orientation = Quaternion() parent_scale = self._parent._getDerivedScale() if self._inherit_scale: self._derived_scale = parent_scale.scale(scale_and_mirror) else: self._derived_scale = scale_and_mirror self._derived_position = parent_orientation.rotate(parent_scale.scale(self._position)) self._derived_position += self._parent._getDerivedPosition() self._world_transformation = Matrix.fromPositionOrientationScale(self._derived_position, self._derived_orientation, self._derived_scale) else: self._derived_position = self._position self._derived_orientation = self._orientation self._derived_scale = scale_and_mirror self._world_transformation = self._transformation def _resetAABB(self): if not self._calculate_aabb: return self._aabb = None if self._aabb_job: self._aabb_job.cancel() self._aabb_job = _CalculateAABBJob(self) self._aabb_job.start()
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 write(self, stream, nodes, mode=MeshWriter.OutputMode.BinaryMode): self._archive = None # Reset archive archive = zipfile.ZipFile(stream, "w", compression=zipfile.ZIP_DEFLATED) try: model_file = zipfile.ZipInfo("3D/3dmodel.model") # Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo. model_file.compress_type = zipfile.ZIP_DEFLATED # Create content types file content_types_file = zipfile.ZipInfo("[Content_Types].xml") content_types_file.compress_type = zipfile.ZIP_DEFLATED content_types = ET.Element("Types", xmlns=self._namespaces["content-types"]) rels_type = ET.SubElement( content_types, "Default", Extension="rels", ContentType= "application/vnd.openxmlformats-package.relationships+xml") model_type = ET.SubElement( content_types, "Default", Extension="model", ContentType= "application/vnd.ms-package.3dmanufacturing-3dmodel+xml") # Create _rels/.rels file relations_file = zipfile.ZipInfo("_rels/.rels") relations_file.compress_type = zipfile.ZIP_DEFLATED relations_element = ET.Element( "Relationships", xmlns=self._namespaces["relationships"]) model_relation_element = ET.SubElement( relations_element, "Relationship", Target="/3D/3dmodel.model", Id="rel0", Type= "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel") savitar_scene = Savitar.Scene() 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 global_container_stack = Application.getInstance( ).getGlobalContainerStack() # 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.preMultiply(translation_matrix) root_node = UM.Application.Application.getInstance().getController( ).getScene().getRoot() for node in nodes: if node == root_node: for root_child in node.getChildren(): savitar_node = self._convertUMNodeToSavitarNode( root_child, transformation_matrix) if savitar_node: savitar_scene.addSceneNode(savitar_node) else: savitar_node = self._convertUMNodeToSavitarNode( node, transformation_matrix) if savitar_node: savitar_scene.addSceneNode(savitar_node) parser = Savitar.ThreeMFParser() scene_string = parser.sceneToString(savitar_scene) archive.writestr(model_file, scene_string) archive.writestr( content_types_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types)) archive.writestr( relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element)) except Exception as e: Logger.logException("e", "Error writing zip file") self.setInformation( catalog.i18nc("@error:zip", "Error writing 3mf file.")) return False finally: if not self._store_archive: archive.close() else: self._archive = archive return True
def _onChangeTimerFinished(self): if not self._enabled: return root = self._controller.getScene().getRoot() for node in BreadthFirstIterator(root): if node is root or type(node) is not SceneNode: continue bbox = node.getBoundingBox() if not bbox or not bbox.isValid(): self._change_timer.start() continue build_volume_bounding_box = copy.deepcopy(self._build_volume.getBoundingBox()) build_volume_bounding_box.setBottom(-9001) # Ignore intersections with the bottom # 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 else: node._outside_buildarea = False # Move it downwards if bottom is above platform move_vector = Vector() if bbox.bottom > 0: move_vector.setY(-bbox.bottom) #if not Float.fuzzyCompare(bbox.bottom, 0.0): # pass#move_vector.setY(-bbox.bottom) # If there is no convex hull for the node, start calculating it and continue. if not node.getDecorator(ConvexHullDecorator): node.addDecorator(ConvexHullDecorator()) if not node.callDecoration("getConvexHull"): if not node.callDecoration("getConvexHullJob"): job = ConvexHullJob.ConvexHullJob(node) job.start() node.callDecoration("setConvexHullJob", job) elif Selection.isSelected(node): pass elif 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 colissions of a group with it's own children if other_node in node.getAllChildren() or node in other_node.getAllChildren(): continue # Ignore colissions within a group if other_node.getParent().callDecoration("isGroup") is not None: if node.getParent().callDecoration("isGroup") is other_node.getParent().callDecoration("isGroup"): continue # Ignore nodes that do not have the right properties set. if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox(): continue # Check to see if the bounding boxes intersect. If not, we can ignore the node as there is no way the hull intersects. if node.getBoundingBox().intersectsBox(other_node.getBoundingBox()) == AxisAlignedBox.IntersectionResult.NoIntersection: continue # Get the overlap distance for both convex hulls. If this returns None, there is no intersection. overlap = node.callDecoration("getConvexHull").intersectsPolygon(other_node.callDecoration("getConvexHull")) if overlap is None: continue move_vector.setX(overlap[0] * 1.1) move_vector.setZ(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 move_vector != Vector(): op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector) op.push()
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() # 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() x, y = 200, 200 arrange_array = ArrangeArray(x=x, y=y, fixed_nodes=[]) arrange_array.add() # Place nodes one at a time start_priority = 0 grouped_operation = GroupedOperation() found_solution_for_all = True left_over_nodes = [] # nodes that do not fit on an empty build plate 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) try_placement = True current_build_plate_number = 0 # always start with the first one # # Only for first build plate # if last_size == size and last_build_plate_number == current_build_plate_number: # # This optimization works if many of the objects have the same size # # Continue with same build plate number # start_priority = last_priority # else: # start_priority = 0 while try_placement: # make sure that current_build_plate_number is not going crazy or you'll have a lot of arrange objects while current_build_plate_number >= arrange_array.count(): arrange_array.add() arranger = arrange_array.get(current_build_plate_number) 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 arranger.place( x, y, hull_shape_arr) # place the object in the arranger node.callDecoration("setBuildPlateNumber", current_build_plate_number) grouped_operation.addOperation( TranslateOperation(node, Vector(x, center_y, y), set_position=True)) try_placement = False else: # very naive, because we skip to the next build plate if one model doesn't fit. if arranger.isEmpty: # apparently we can never place this object left_over_nodes.append(node) try_placement = False else: # try next build plate current_build_plate_number += 1 try_placement = True status_message.setProgress((idx + 1) / len(nodes_arr) * 100) Job.yieldThread() for node in left_over_nodes: node.callDecoration("setBuildPlateNumber", -1) # these are not on any build plate found_solution_for_all = False 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 read(self, file_name): result = None extension = os.path.splitext(file_name)[1] if extension.lower() == self._supported_extension: result = SceneNode() # The base object of 3mf is a zipped archive. archive = zipfile.ZipFile(file_name, 'r') try: root = ET.parse(archive.open("3D/3dmodel.model")) # There can be multiple objects, try to load all of them. objects = root.findall("./3mf:resources/3mf:object", self._namespaces) if len(objects) == 0: Logger.log("w", "No objects found in 3MF file %s, either the file is corrupt or you are using an outdated format", file_name) return None for object in objects: mesh = MeshData() node = SceneNode() vertex_list = [] #for vertex in object.mesh.vertices.vertex: for vertex in object.findall(".//3mf:vertex", self._namespaces): vertex_list.append([vertex.get("x"), vertex.get("y"), vertex.get("z")]) Job.yieldThread() triangles = object.findall(".//3mf:triangle", self._namespaces) mesh.reserveFaceCount(len(triangles)) #for triangle in object.mesh.triangles.triangle: for triangle in triangles: v1 = int(triangle.get("v1")) v2 = int(triangle.get("v2")) v3 = int(triangle.get("v3")) mesh.addFace(vertex_list[v1][0],vertex_list[v1][1],vertex_list[v1][2],vertex_list[v2][0],vertex_list[v2][1],vertex_list[v2][2],vertex_list[v3][0],vertex_list[v3][1],vertex_list[v3][2]) Job.yieldThread() #TODO: We currently do not check for normals and simply recalculate them. mesh.calculateNormals() node.setMeshData(mesh) node.setSelectable(True) transformation = root.findall("./3mf:build/3mf:item[@objectid='{0}']".format(object.get("id")), self._namespaces) if transformation: transformation = transformation[0] if transformation.get("transform"): splitted_transformation = transformation.get("transform").split() ## Transformation is saved as: ## M00 M01 M02 0.0 ## M10 M11 M12 0.0 ## M20 M21 M22 0.0 ## M30 M31 M32 1.0 ## We switch the row & cols as that is how everyone else uses matrices! temp_mat = Matrix() # Rotation & Scale temp_mat._data[0,0] = splitted_transformation[0] temp_mat._data[1,0] = splitted_transformation[1] temp_mat._data[2,0] = splitted_transformation[2] temp_mat._data[0,1] = splitted_transformation[3] temp_mat._data[1,1] = splitted_transformation[4] temp_mat._data[2,1] = splitted_transformation[5] temp_mat._data[0,2] = splitted_transformation[6] temp_mat._data[1,2] = splitted_transformation[7] temp_mat._data[2,2] = splitted_transformation[8] # Translation temp_mat._data[0,3] = splitted_transformation[9] temp_mat._data[1,3] = splitted_transformation[10] temp_mat._data[2,3] = splitted_transformation[11] node.setPosition(Vector(temp_mat.at(0,3), temp_mat.at(1,3), temp_mat.at(2,3))) temp_quaternion = Quaternion() temp_quaternion.setByMatrix(temp_mat) node.setOrientation(temp_quaternion) # Magical scale extraction S2 = temp_mat.getTransposed().multiply(temp_mat) scale_x = math.sqrt(S2.at(0,0)) scale_y = math.sqrt(S2.at(1,1)) scale_z = math.sqrt(S2.at(2,2)) node.setScale(Vector(scale_x,scale_y,scale_z)) # We use a different coordinate frame, so rotate. #rotation = Quaternion.fromAngleAxis(-0.5 * math.pi, Vector(1,0,0)) #node.rotate(rotation) result.addChild(node) Job.yieldThread() #If there is more then one object, group them. try: if len(objects) > 1: group_decorator = GroupDecorator() result.addDecorator(group_decorator) except: pass except Exception as e: Logger.log("e" ,"exception occured in 3mf reader: %s" , e) return result