def beginRendering(self): scene = self.getController().getScene() renderer = self.getRenderer() if not self._theme: self._theme = Application.getInstance().getTheme() if not self._enabled_shader: self._enabled_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "overhang.shader")) self._enabled_shader.setUniformValue("u_overhangColor", Color(*self._theme.getColor("model_overhang").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(*self._theme.getColor("model_unslicable").getRgb())) self._disabled_shader.setUniformValue("u_diffuseColor2", Color(*self._theme.getColor("model_unslicable_alt").getRgb())) self._disabled_shader.setUniformValue("u_width", 50.0) if not self._non_printing_shader: self._non_printing_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "transparent_object.shader")) self._non_printing_shader.setUniformValue("u_diffuseColor", Color(*self._theme.getColor("model_non_printing").getRgb())) self._non_printing_shader.setUniformValue("u_opacity", 0.6) global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack: support_extruder_nr = global_container_stack.getProperty("support_extruder_nr", "value") support_angle_stack = Application.getInstance().getExtruderManager().getExtruderStack(support_extruder_nr) if support_angle_stack is not None and Preferences.getInstance().getValue("view/show_overhang"): angle = support_angle_stack.getProperty("support_angle", "value") # Make sure the overhang angle is valid before passing it to the shader # Note: if the overhang angle is set to its default value, it does not need to get validated (validationState = None) if angle is not None and global_container_stack.getProperty("support_angle", "validationState") in [None, ValidatorState.Valid]: self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(90 - angle))) else: self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0))) #Overhang angle of 0 causes no area at all to be marked as overhang. else: self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0))) for node in DepthFirstIterator(scene.getRoot()): if not node.render(renderer): if node.getMeshData() and node.isVisible(): uniforms = {} shade_factor = 1.0 per_mesh_stack = node.callDecoration("getStack") # Get color to render this mesh in from ExtrudersModel extruder_index = 0 extruder_id = node.callDecoration("getActiveExtruder") if extruder_id: extruder_index = max(0, self._extruders_model.find("id", extruder_id)) # Use the support extruder instead of the active extruder if this is a support_mesh if per_mesh_stack: if per_mesh_stack.getProperty("support_mesh", "value"): extruder_index = int(global_container_stack.getProperty("support_extruder_nr", "value")) try: material_color = self._extruders_model.getItem(extruder_index)["color"] except KeyError: material_color = self._extruders_model.defaultColors[0] if extruder_index != ExtruderManager.getInstance().activeExtruderIndex: # Shade objects that are printed with the non-active extruder 25% darker shade_factor = 0.6 try: # Colors are passed as rgb hex strings (eg "#ffffff"), and the shader needs # an rgba list of floats (eg [1.0, 1.0, 1.0, 1.0]) uniforms["diffuse_color"] = [ shade_factor * int(material_color[1:3], 16) / 255, shade_factor * int(material_color[3:5], 16) / 255, shade_factor * int(material_color[5:7], 16) / 255, 1.0 ] except ValueError: pass if node.callDecoration("isNonPrintingMesh"): if per_mesh_stack and (per_mesh_stack.getProperty("infill_mesh", "value") or per_mesh_stack.getProperty("cutting_mesh", "value")): renderer.queueNode(node, shader = self._non_printing_shader, uniforms = uniforms, transparent = True) else: renderer.queueNode(node, shader = self._non_printing_shader, transparent = True) elif getattr(node, "_outside_buildarea", False): renderer.queueNode(node, shader = self._disabled_shader) else: renderer.queueNode(node, shader = self._enabled_shader, uniforms = uniforms) if node.callDecoration("isGroup") and Selection.isSelected(node): renderer.queueNode(scene.getRoot(), mesh = node.getBoundingBoxMesh(), mode = RenderBatch.RenderMode.LineLoop)
def event(self, event): if event.type == Event.ViewActivateEvent: # FIX: on Max OS X, somehow QOpenGLContext.currentContext() can become None during View switching. # This can happen when you do the following steps: # 1. Start Cura # 2. Load a model # 3. Switch to Custom mode # 4. Select the model and click on the per-object tool icon # 5. Switch view to Layer view or X-Ray # 6. Cura will very likely crash # It seems to be a timing issue that the currentContext can somehow be empty, but I have no clue why. # This fix tries to reschedule the view changing event call on the Qt thread again if the current OpenGL # context is None. if Platform.isOSX(): if QOpenGLContext.currentContext() is None: Logger.log( "d", "current context of OpenGL is empty on Mac OS X, will try to create shaders later" ) CuraApplication.getInstance().callLater( lambda e=event: self.event(e)) return if not self._xray_pass: # Currently the RenderPass constructor requires a size > 0 # This should be fixed in RenderPass's constructor. self._xray_pass = XRayPass.XRayPass(1, 1) self.getRenderer().addRenderPass(self._xray_pass) if not self._xray_composite_shader: self._xray_composite_shader = OpenGL.getInstance( ).createShaderProgram( Resources.getPath(Resources.Shaders, "xray_composite.shader")) theme = Application.getInstance().getTheme() self._xray_composite_shader.setUniformValue( "u_background_color", Color(*theme.getColor("viewport_background").getRgb())) self._xray_composite_shader.setUniformValue( "u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb())) self._xray_composite_shader.setUniformValue( "u_flat_error_color_mix", 1.) # Show flat error color _only_ in xray-view. if not self._composite_pass: self._composite_pass = self.getRenderer().getRenderPass( "composite") self._old_layer_bindings = self._composite_pass.getLayerBindings() self._composite_pass.setLayerBindings( ["default", "selection", "xray"]) self._old_composite_shader = self._composite_pass.getCompositeShader( ) self._composite_pass.setCompositeShader( self._xray_composite_shader) if event.type == Event.ViewDeactivateEvent: self.getRenderer().removeRenderPass(self._xray_pass) self._composite_pass.setLayerBindings(self._old_layer_bindings) self._composite_pass.setCompositeShader(self._old_composite_shader)
def beginRendering(self): scene = self.getController().getScene() renderer = self.getRenderer() if not self._selection_shader: self._selection_shader = OpenGL.getInstance().createShaderProgram( Resources.getPath(Resources.Shaders, "default.shader")) self._selection_shader.setUniformValue("u_color", Color(32, 32, 32, 128)) for node in DepthFirstIterator(scene.getRoot()): # We do not want to render ConvexHullNode as it conflicts with the bottom layers. # However, it is somewhat relevant when the node is selected, so do render it then. if type(node) is ConvexHullNode and not Selection.isSelected( node.getWatchedNode()): continue if not node.render(renderer): if node.getMeshData() and node.isVisible(): if Selection.isSelected(node): renderer.queueNode(node, transparent=True, shader=self._selection_shader) 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._current_layer_num - self._solid_layers > -1: start = 0 end = 0 element_counts = layer_data.getElementCounts() for layer, counts in element_counts.items(): if layer + self._solid_layers > self._current_layer_num: break end += counts # This uses glDrawRangeElements internally to only draw a certain range of lines. renderer.queueNode(node, mesh=layer_data, mode=RenderBatch.RenderMode.Lines, range=(start, end)) # We currently recreate the current "solid" layers every time a if not self._current_layer_mesh: self._current_layer_mesh = MeshData() for i in range(self._solid_layers): layer = self._current_layer_num - i if layer < 0: continue try: layer_mesh = layer_data.getLayer( layer).createMesh() if not layer_mesh or layer_mesh.getVertices( ) is None: continue except: continue if self._current_layer_mesh: #Threading thing; Switching between views can cause the current layer mesh to be deleted. self._current_layer_mesh.addVertices( layer_mesh.getVertices()) # Scale layer color by a brightness factor based on the current layer number # This will result in a range of 0.5 - 1.0 to multiply colors by. brightness = (2.0 - (i / self._solid_layers)) / 2.0 if self._current_layer_mesh: self._current_layer_mesh.addColors( layer_mesh.getColors() * brightness) if self._current_layer_mesh: renderer.queueNode(node, mesh=self._current_layer_mesh) if not self._current_layer_jumps: self._current_layer_jumps = MeshData() for i in range(1): layer = self._current_layer_num - i if layer < 0: continue try: layer_mesh = layer_data.getLayer( layer).createJumps() if not layer_mesh or layer_mesh.getVertices( ) is None: continue except: continue self._current_layer_jumps.addVertices( layer_mesh.getVertices()) # Scale layer color by a brightness factor based on the current layer number # This will result in a range of 0.5 - 1.0 to multiply colors by. brightness = (2.0 - (i / self._solid_layers)) / 2.0 self._current_layer_jumps.addColors( layer_mesh.getColors() * brightness) renderer.queueNode(node, mesh=self._current_layer_jumps)
def beginRendering(self): scene = self.getController().getScene() renderer = self.getRenderer() if not self._enabled_material: if Preferences.getInstance().getValue("view/show_overhang"): self._enabled_material = renderer.createMaterial( Resources.getPath(Resources.Shaders, "default.vert"), Resources.getPath(Resources.Shaders, "overhang.frag")) else: self._enabled_material = renderer.createMaterial( Resources.getPath(Resources.Shaders, "default.vert"), Resources.getPath(Resources.Shaders, "default.frag")) self._enabled_material.setUniformValue("u_ambientColor", Color(0.3, 0.3, 0.3, 1.0)) self._enabled_material.setUniformValue("u_diffuseColor", self.EnabledColor) self._enabled_material.setUniformValue("u_specularColor", Color(0.4, 0.4, 0.4, 1.0)) self._enabled_material.setUniformValue("u_overhangColor", Color(1.0, 0.0, 0.0, 1.0)) self._enabled_material.setUniformValue("u_shininess", 20.) if not self._disabled_material: self._disabled_material = renderer.createMaterial( Resources.getPath(Resources.Shaders, "default.vert"), Resources.getPath(Resources.Shaders, "default.frag")) self._disabled_material.setUniformValue("u_ambientColor", Color(0.3, 0.3, 0.3, 1.0)) self._disabled_material.setUniformValue("u_diffuseColor", self.DisabledColor) self._disabled_material.setUniformValue("u_specularColor", Color(0.4, 0.4, 0.4, 1.0)) self._disabled_material.setUniformValue("u_overhangColor", Color(1.0, 0.0, 0.0, 1.0)) self._disabled_material.setUniformValue("u_shininess", 20.) if Application.getInstance().getMachineManager().getActiveProfile(): profile = Application.getInstance().getMachineManager( ).getActiveProfile() if profile.getSettingValue("support_enable"): angle = profile.getSettingValue("support_angle") if angle != None: self._enabled_material.setUniformValue( "u_overhangAngle", math.cos(math.radians(90 - angle))) else: self._enabled_material.setUniformValue( "u_overhangAngle", math.cos(math.radians(0))) for node in DepthFirstIterator(scene.getRoot()): if not node.render(renderer): if node.getMeshData() and node.isVisible(): # TODO: Find a better way to handle this #if node.getBoundingBoxMesh(): # renderer.queueNode(scene.getRoot(), mesh = node.getBoundingBoxMesh(),mode = Renderer.RenderLines) if hasattr(node, "_outside_buildarea"): if node._outside_buildarea: renderer.queueNode( node, material=self._disabled_material) else: renderer.queueNode(node, material=self._enabled_material) else: renderer.queueNode(node, material=self._enabled_material) if node.callDecoration("isGroup"): renderer.queueNode(scene.getRoot(), mesh=node.getBoundingBoxMesh(), mode=Renderer.RenderLines)
def _dropAlpha(self, color): return Color(color.r, color.g, color.b, 0.0)
def rebuild(self): if not self._width or not self._height or not self._depth: return min_w = -self._width / 2 max_w = self._width / 2 min_h = 0.0 max_h = self._height min_d = -self._depth / 2 max_d = self._depth / 2 mb = MeshBuilder() # Outline 'cube' of the build volume mb.addLine(Vector(min_w, min_h, min_d), Vector(max_w, min_h, min_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, max_h, min_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, max_h, min_d), Vector(max_w, max_h, min_d), color=self.VolumeOutlineColor) mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, max_h, min_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, max_d), Vector(max_w, min_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, max_d), Vector(min_w, max_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, max_h, max_d), Vector(max_w, max_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(max_w, min_h, max_d), Vector(max_w, max_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, min_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, min_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color=self.VolumeOutlineColor) self.setMeshData(mb.build()) mb = MeshBuilder() mb.addQuad(Vector(min_w, min_h - 0.2, min_d), Vector(max_w, min_h - 0.2, min_d), Vector(max_w, min_h - 0.2, max_d), Vector(min_w, min_h - 0.2, max_d)) for n in range(0, 6): v = mb.getVertex(n) mb.setVertexUVCoordinates(n, v[0], v[2]) self._grid_mesh = mb.build() disallowed_area_height = 0.1 disallowed_area_size = 0 if self._disallowed_areas: mb = MeshBuilder() color = Color(0.0, 0.0, 0.0, 0.15) for polygon in self._disallowed_areas: points = polygon.getPoints() first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) previous_point = Vector( self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) for point in points: new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height, self._clamp(point[1], min_d, max_d)) mb.addFace(first, previous_point, new_point, color=color) previous_point = new_point # Find the largest disallowed area to exclude it from the maximum scale bounds. # This is a very nasty hack. This pretty much only works for UM machines. # This disallowed area_size needs a -lot- of rework at some point in the future: TODO if numpy.min( points[:, 1] ) >= 0: # This filters out all areas that have points to the left of the centre. This is done to filter the skirt area. size = abs( numpy.max(points[:, 1]) - numpy.min(points[:, 1])) else: size = 0 disallowed_area_size = max(size, disallowed_area_size) self._disallowed_area_mesh = mb.build() else: self._disallowed_area_mesh = None self._volume_aabb = AxisAlignedBox( minimum=Vector(min_w, min_h - 1.0, min_d), maximum=Vector(max_w, max_h - self._raft_thickness, max_d)) bed_adhesion_size = 0.0 container_stack = Application.getInstance().getGlobalContainerStack() if container_stack: bed_adhesion_size = self._getBedAdhesionSize(container_stack) # As this works better for UM machines, we only add the disallowed_area_size for the z direction. # This is probably wrong in all other cases. TODO! # The +1 and -1 is added as there is always a bit of extra room required to work properly. scale_to_max_bounds = AxisAlignedBox( minimum=Vector( min_w + bed_adhesion_size + 1, min_h, min_d + disallowed_area_size - bed_adhesion_size + 1), maximum=Vector( max_w - bed_adhesion_size - 1, max_h - self._raft_thickness, max_d - disallowed_area_size + bed_adhesion_size - 1)) Application.getInstance().getController().getScene( )._maximum_bounds = scale_to_max_bounds
def event(self, event) -> bool: modifiers = QApplication.keyboardModifiers() ctrl_is_active = modifiers & Qt.ControlModifier shift_is_active = modifiers & Qt.ShiftModifier if event.type == Event.KeyPressEvent and ctrl_is_active: amount = 10 if shift_is_active else 1 if event.key == KeyEvent.UpKey: self.setLayer(self._current_layer_num + amount) return True if event.key == KeyEvent.DownKey: self.setLayer(self._current_layer_num - amount) return True if event.type == Event.ViewActivateEvent: # Start listening to changes. Application.getInstance().getPreferences( ).preferenceChanged.connect(self._onPreferencesChanged) self._controller.getScene().getRoot().childrenChanged.connect( self._onSceneChanged) self.calculateColorSchemeLimits() self.calculateMaxLayers() self.calculateMaxPathsOnLayer(self._current_layer_num) # FIX: on Max OS X, somehow QOpenGLContext.currentContext() can become None during View switching. # This can happen when you do the following steps: # 1. Start Cura # 2. Load a model # 3. Switch to Custom mode # 4. Select the model and click on the per-object tool icon # 5. Switch view to Layer view or X-Ray # 6. Cura will very likely crash # It seems to be a timing issue that the currentContext can somehow be empty, but I have no clue why. # This fix tries to reschedule the view changing event call on the Qt thread again if the current OpenGL # context is None. if Platform.isOSX(): if QOpenGLContext.currentContext() is None: Logger.log( "d", "current context of OpenGL is empty on Mac OS X, will try to create shaders later" ) CuraApplication.getInstance().callLater( lambda e=event: self.event(e)) return False # Make sure the SimulationPass is created layer_pass = self.getSimulationPass() renderer = self.getRenderer() if renderer is None: return False renderer.addRenderPass(layer_pass) # Make sure the NozzleNode is add to the root nozzle = self.getNozzleNode() nozzle.setParent(self.getController().getScene().getRoot()) nozzle.setVisible(False) Application.getInstance().globalContainerStackChanged.connect( self._onGlobalStackChanged) self._onGlobalStackChanged() if not self._simulationview_composite_shader: plugin_path = cast( str, PluginRegistry.getInstance().getPluginPath( "SimulationView")) self._simulationview_composite_shader = OpenGL.getInstance( ).createShaderProgram( os.path.join(plugin_path, "simulationview_composite.shader")) theme = CuraApplication.getInstance().getTheme() if theme is not None: self._simulationview_composite_shader.setUniformValue( "u_background_color", Color(*theme.getColor("viewport_background").getRgb())) self._simulationview_composite_shader.setUniformValue( "u_outline_color", Color(*theme.getColor( "model_selection_outline").getRgb())) if not self._composite_pass: self._composite_pass = cast( CompositePass, renderer.getRenderPass("composite")) self._old_layer_bindings = self._composite_pass.getLayerBindings( )[:] # make a copy so we can restore to it later self._composite_pass.getLayerBindings().append("simulationview") self._old_composite_shader = self._composite_pass.getCompositeShader( ) self._composite_pass.setCompositeShader( self._simulationview_composite_shader) self._updateSliceWarningVisibility() elif event.type == Event.ViewDeactivateEvent: self._controller.getScene().getRoot().childrenChanged.disconnect( self._onSceneChanged) Application.getInstance().getPreferences( ).preferenceChanged.disconnect(self._onPreferencesChanged) self._wireprint_warning_message.hide() self._slice_first_warning_message.hide() Application.getInstance().globalContainerStackChanged.disconnect( self._onGlobalStackChanged) if self._global_container_stack: self._global_container_stack.propertyChanged.disconnect( self._onPropertyChanged) if self._nozzle_node: self._nozzle_node.setParent(None) renderer = self.getRenderer() if renderer is None: return False if self._layer_pass is not None: renderer.removeRenderPass(self._layer_pass) if self._composite_pass: self._composite_pass.setLayerBindings( cast(List[str], self._old_layer_bindings)) self._composite_pass.setCompositeShader( cast(ShaderProgram, self._old_composite_shader)) return False
class LayerPolygon: NoneType = 0 Inset0Type = 1 InsetXType = 2 SkinType = 3 SupportType = 4 SkirtType = 5 InfillType = 6 SupportInfillType = 7 MoveCombingType = 8 MoveRetractionType = 9 __jump_map = numpy.logical_or( numpy.arange(10) == NoneType, numpy.arange(10) >= MoveCombingType) def __init__(self, mesh, extruder, line_types, data, line_widths): self._mesh = mesh self._extruder = extruder self._types = line_types self._data = data self._line_widths = line_widths self._vertex_begin = 0 self._vertex_end = 0 self._index_begin = 0 self._index_end = 0 self._jump_mask = self.__jump_map[self._types] self._jump_count = numpy.sum(self._jump_mask) self._mesh_line_count = len(self._types) - self._jump_count self._vertex_count = self._mesh_line_count + numpy.sum( self._types[1:] == self._types[:-1]) # Buffering the colors shouldn't be necessary as it is not # re-used and can save alot of memory usage. self._colors = self.__color_map[self._types] self._color_map = self.__color_map # When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType # Should be generated in better way, not hardcoded. self._isInfillOrSkinTypeMap = numpy.array( [0, 0, 0, 1, 0, 0, 1, 1, 0, 0], dtype=numpy.bool) self._build_cache_line_mesh_mask = None self._build_cache_needed_points = None def buildCache(self): # For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out. self._build_cache_line_mesh_mask = numpy.logical_not( numpy.logical_or(self._jump_mask, self._types == LayerPolygon.InfillType)) mesh_line_count = numpy.sum(self._build_cache_line_mesh_mask) self._index_begin = 0 self._index_end = mesh_line_count self._build_cache_needed_points = numpy.ones((len(self._types), 2), dtype=numpy.bool) # Only if the type of line segment changes do we need to add an extra vertex to change colors self._build_cache_needed_points[ 1:, 0][:, numpy.newaxis] = self._types[1:] != self._types[:-1] # Mark points as unneeded if they are of types we don't want in the line mesh according to the calculated mask numpy.logical_and(self._build_cache_needed_points, self._build_cache_line_mesh_mask, self._build_cache_needed_points) self._vertex_begin = 0 self._vertex_end = numpy.sum(self._build_cache_needed_points) def build(self, vertex_offset, index_offset, vertices, colors, indices): if (self._build_cache_line_mesh_mask is None) or (self._build_cache_needed_points is None): self.buildCache() line_mesh_mask = self._build_cache_line_mesh_mask needed_points_list = self._build_cache_needed_points # Index to the points we need to represent the line mesh. This is constructed by generating simple # start and end points for each line. For line segment n these are points n and n+1. Row n reads [n n+1] # Then then the indices for the points we don't need are thrown away based on the pre-calculated list. index_list = (numpy.arange(len(self._types)).reshape( (-1, 1)) + numpy.array([[0, 1]])).reshape( (-1, 1))[needed_points_list.reshape((-1, 1))] # The relative values of begin and end indices have already been set in buildCache, so we only need to offset them to the parents offset. self._vertex_begin += vertex_offset self._vertex_end += vertex_offset # Points are picked based on the index list to get the vertices needed. vertices[self._vertex_begin:self._vertex_end, :] = self._data[ index_list, :] # Create an array with colors for each vertex and remove the color data for the points that has been thrown away. colors[self._vertex_begin:self._vertex_end, :] = numpy.tile( self._colors, (1, 2)).reshape((-1, 4))[needed_points_list.ravel()] colors[self._vertex_begin:self._vertex_end, :] *= numpy.array( [[0.5, 0.5, 0.5, 1.0]], numpy.float32) # The relative values of begin and end indices have already been set in buildCache, so we only need to offset them to the parents offset. self._index_begin += index_offset self._index_end += index_offset indices[self._index_begin:self._index_end, :] = numpy.arange( self._index_end - self._index_begin, dtype=numpy.int32).reshape( (-1, 1)) # When the line type changes the index needs to be increased by 2. indices[self._index_begin:self._index_end, :] += numpy.cumsum( needed_points_list[line_mesh_mask.ravel(), 0], dtype=numpy.int32).reshape((-1, 1)) # Each line segment goes from it's starting point p to p+1, offset by the vertex index. # The -1 is to compensate for the neccecarily True value of needed_points_list[0,0] which causes an unwanted +1 in cumsum above. indices[self._index_begin:self._index_end, :] += numpy.array( [self._vertex_begin - 1, self._vertex_begin]) self._build_cache_line_mesh_mask = None self._build_cache_needed_points = None def getColors(self): return self._colors def mapLineTypeToColor(self, line_types): return self._color_map[line_types] def isInfillOrSkinType(self, line_types): return self._isInfillOrSkinTypeMap[line_types] def lineMeshVertexCount(self): return (self._vertex_end - self._vertex_begin) def lineMeshElementCount(self): return (self._index_end - self._index_begin) @property def extruder(self): return self._extruder @property def types(self): return self._types @property def data(self): return self._data @property def elementCount(self): return ( self._index_end - self._index_begin ) * 2 # The range of vertices multiplied by 2 since each vertex is used twice @property def lineWidths(self): return self._line_widths @property def jumpMask(self): return self._jump_mask @property def meshLineCount(self): return self._mesh_line_count @property def jumpCount(self): return self._jump_count # Calculate normals for the entire polygon using numpy. def getNormals(self): normals = numpy.copy(self._data) normals[:, 1] = 0.0 # We are only interested in 2D normals # Calculate the edges between points. # The call to numpy.roll shifts the entire array by one so that # we end up subtracting each next point from the current, wrapping # around. This gives us the edges from the next point to the current # point. normals = numpy.diff(normals, 1, 0) # Calculate the length of each edge using standard Pythagoras lengths = numpy.sqrt(normals[:, 0]**2 + normals[:, 2]**2) # The normal of a 2D vector is equal to its x and y coordinates swapped # and then x inverted. This code does that. normals[:, [0, 2]] = normals[:, [2, 0]] normals[:, 0] *= -1 # Normalize the normals. normals[:, 0] /= lengths normals[:, 2] /= lengths return normals __color_mapping = { NoneType: Color(1.0, 1.0, 1.0, 1.0), Inset0Type: Color(1.0, 0.0, 0.0, 1.0), InsetXType: Color(0.0, 1.0, 0.0, 1.0), SkinType: Color(1.0, 1.0, 0.0, 1.0), SupportType: Color(0.0, 1.0, 1.0, 1.0), SkirtType: Color(0.0, 1.0, 1.0, 1.0), InfillType: Color(1.0, 0.74, 0.0, 1.0), SupportInfillType: Color(0.0, 1.0, 1.0, 1.0), MoveCombingType: Color(0.0, 0.0, 1.0, 1.0), MoveRetractionType: Color(0.5, 0.5, 1.0, 1.0), } # Should be generated in better way, not hardcoded. __color_map = numpy.array([[1.0, 1.0, 1.0, 1.0], [1.0, 0.0, 0.0, 1.0], [0.0, 1.0, 0.0, 1.0], [1.0, 1.0, 0.0, 1.0], [0.0, 1.0, 1.0, 1.0], [0.0, 1.0, 1.0, 1.0], [1.0, 0.74, 0.0, 1.0], [0.0, 1.0, 1.0, 1.0], [0.0, 0.0, 1.0, 1.0], [0.5, 0.5, 1.0, 1.0]])
class ToolHandle(SceneNode.SceneNode): NoAxis = 1 XAxis = 2 YAxis = 3 ZAxis = 4 AllAxis = 5 # These colors are used to draw the selection pass only. They must be unique, which is # why we cannot rely on themed colors DisabledSelectionColor = Color(0.5, 0.5, 0.5, 1.0) XAxisSelectionColor = Color(1.0, 0.0, 0.0, 1.0) YAxisSelectionColor = Color(0.0, 0.0, 1.0, 1.0) ZAxisSelectionColor = Color(0.0, 1.0, 0.0, 1.0) AllAxisSelectionColor = Color(1.0, 1.0, 1.0, 1.0) def __init__(self, parent=None): super().__init__(parent) self._disabled_axis_color = None self._x_axis_color = None self._y_axis_color = None self._z_axis_color = None self._all_axis_color = None self._axis_color_map = {} self._scene = Application.getInstance().getController().getScene() self._solid_mesh = None self._line_mesh = None self._selection_mesh = None self._shader = None self._previous_dist = None self._active_axis = None self._auto_scale = True self.setCalculateBoundingBox(False) Selection.selectionCenterChanged.connect( self._onSelectionCenterChanged) Application.getInstance().engineCreatedSignal.connect( self._onEngineCreated) def getLineMesh(self): return self._line_mesh def setLineMesh(self, mesh): self._line_mesh = mesh self.meshDataChanged.emit(self) def getSolidMesh(self): return self._solid_mesh def setSolidMesh(self, mesh): self._solid_mesh = mesh self.meshDataChanged.emit(self) def getSelectionMesh(self): return self._selection_mesh def setSelectionMesh(self, mesh): self._selection_mesh = mesh self.meshDataChanged.emit(self) def getMaterial(self): return self._shader def render(self, renderer): if not self._shader: self._shader = OpenGL.getInstance().createShaderProgram( Resources.getPath(Resources.Shaders, "toolhandle.shader")) if self._auto_scale: camera_position = self._scene.getActiveCamera().getWorldPosition() dist = (camera_position - self.getWorldPosition()).length() scale = dist / 400 self.setScale(Vector(scale, scale, scale)) if self._line_mesh: renderer.queueNode(self, mesh=self._line_mesh, mode=RenderBatch.RenderMode.Lines, overlay=True, shader=self._shader) if self._solid_mesh: renderer.queueNode(self, mesh=self._solid_mesh, overlay=True, shader=self._shader) return True def setActiveAxis(self, axis): if axis == self._active_axis or not self._shader: return if axis: self._shader.setUniformValue("u_activeColor", self._axis_color_map[axis]) else: self._shader.setUniformValue("u_activeColor", self._disabled_axis_color) self._active_axis = axis self._scene.sceneChanged.emit(self) def isAxis(self, value): return value in self._axis_color_map def buildMesh(self): # This method should be overridden by toolhandle implementations pass def _onSelectionCenterChanged(self): self.setPosition(Selection.getSelectionCenter()) def _onEngineCreated(self): theme = Application.getInstance().getTheme() self._disabled_axis_color = Color( *theme.getColor("disabled_axis").getRgb()) self._x_axis_color = Color(*theme.getColor("x_axis").getRgb()) self._y_axis_color = Color(*theme.getColor("y_axis").getRgb()) self._z_axis_color = Color(*theme.getColor("z_axis").getRgb()) self._all_axis_color = Color(*theme.getColor("all_axis").getRgb()) self._axis_color_map = { self.NoAxis: self._disabled_axis_color, self.XAxis: self._x_axis_color, self.YAxis: self._y_axis_color, self.ZAxis: self._z_axis_color, self.AllAxis: self._all_axis_color } self.buildMesh()
class BuildVolume(SceneNode): VolumeOutlineColor = Color(12, 169, 227, 255) def __init__(self, parent=None): super().__init__(parent) self._width = 0 self._height = 0 self._depth = 0 self._shader = None self._grid_mesh = None self._grid_shader = None self._disallowed_areas = [] self._disallowed_area_mesh = None self.setCalculateBoundingBox(False) self._active_profile = None self._active_instance = None Application.getInstance().getMachineManager( ).activeMachineInstanceChanged.connect(self._onActiveInstanceChanged) self._onActiveInstanceChanged() Application.getInstance().getMachineManager( ).activeProfileChanged.connect(self._onActiveProfileChanged) self._onActiveProfileChanged() def setWidth(self, width): if width: self._width = width def setHeight(self, height): if height: self._height = height def setDepth(self, depth): if depth: self._depth = depth def getDisallowedAreas(self): return self._disallowed_areas def setDisallowedAreas(self, areas): self._disallowed_areas = areas def render(self, renderer): if not self.getMeshData(): return True if not self._shader: self._shader = OpenGL.getInstance().createShaderProgram( Resources.getPath(Resources.Shaders, "default.shader")) self._grid_shader = OpenGL.getInstance().createShaderProgram( Resources.getPath(Resources.Shaders, "grid.shader")) renderer.queueNode(self, mode=RenderBatch.RenderMode.Lines) renderer.queueNode(self, mesh=self._grid_mesh, shader=self._grid_shader, backface_cull=True) if self._disallowed_area_mesh: renderer.queueNode(self, mesh=self._disallowed_area_mesh, shader=self._shader, transparent=True, backface_cull=True, sort=-9) return True def rebuild(self): if self._width == 0 or self._height == 0 or self._depth == 0: return min_w = -self._width / 2 max_w = self._width / 2 min_h = 0.0 max_h = self._height min_d = -self._depth / 2 max_d = self._depth / 2 mb = MeshBuilder() mb.addLine(Vector(min_w, min_h, min_d), Vector(max_w, min_h, min_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, max_h, min_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, max_h, min_d), Vector(max_w, max_h, min_d), color=self.VolumeOutlineColor) mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, max_h, min_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, max_d), Vector(max_w, min_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, max_d), Vector(min_w, max_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, max_h, max_d), Vector(max_w, max_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(max_w, min_h, max_d), Vector(max_w, max_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, min_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, min_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color=self.VolumeOutlineColor) self.setMeshData(mb.getData()) mb = MeshBuilder() mb.addQuad(Vector(min_w, min_h - 0.2, min_d), Vector(max_w, min_h - 0.2, min_d), Vector(max_w, min_h - 0.2, max_d), Vector(min_w, min_h - 0.2, max_d)) self._grid_mesh = mb.getData() for n in range(0, 6): v = self._grid_mesh.getVertex(n) self._grid_mesh.setVertexUVCoordinates(n, v[0], v[2]) disallowed_area_height = 0.1 disallowed_area_size = 0 if self._disallowed_areas: mb = MeshBuilder() color = Color(0.0, 0.0, 0.0, 0.15) for polygon in self._disallowed_areas: points = polygon.getPoints() first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) previous_point = Vector( self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) for point in points: new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height, self._clamp(point[1], min_d, max_d)) mb.addFace(first, previous_point, new_point, color=color) previous_point = new_point # Find the largest disallowed area to exclude it from the maximum scale bounds size = abs(numpy.max(points[:, 1]) - numpy.min(points[:, 1])) disallowed_area_size = max(size, disallowed_area_size) self._disallowed_area_mesh = mb.getData() else: self._disallowed_area_mesh = None self._aabb = AxisAlignedBox(minimum=Vector(min_w, min_h - 1.0, min_d), maximum=Vector(max_w, max_h, max_d)) skirt_size = 0.0 profile = Application.getInstance().getMachineManager( ).getActiveProfile() if profile: skirt_size = self._getSkirtSize(profile) scale_to_max_bounds = AxisAlignedBox( minimum=Vector(min_w + skirt_size, min_h, min_d + skirt_size + disallowed_area_size), maximum=Vector(max_w - skirt_size, max_h, max_d - skirt_size - disallowed_area_size)) Application.getInstance().getController().getScene( )._maximum_bounds = scale_to_max_bounds def _onActiveInstanceChanged(self): self._active_instance = Application.getInstance().getMachineManager( ).getActiveMachineInstance() if self._active_instance: self._width = self._active_instance.getMachineSettingValue( "machine_width") self._height = self._active_instance.getMachineSettingValue( "machine_height") self._depth = self._active_instance.getMachineSettingValue( "machine_depth") self._updateDisallowedAreas() self.rebuild() def _onActiveProfileChanged(self): if self._active_profile: self._active_profile.settingValueChanged.disconnect( self._onSettingValueChanged) self._active_profile = Application.getInstance().getMachineManager( ).getActiveProfile() if self._active_profile: self._active_profile.settingValueChanged.connect( self._onSettingValueChanged) self._updateDisallowedAreas() self.rebuild() def _onSettingValueChanged(self, setting): if setting in self._skirt_settings: self._updateDisallowedAreas() self.rebuild() def _updateDisallowedAreas(self): if not self._active_instance or not self._active_profile: return disallowed_areas = self._active_instance.getMachineSettingValue( "machine_disallowed_areas") areas = [] skirt_size = 0.0 if self._active_profile: skirt_size = self._getSkirtSize(self._active_profile) if disallowed_areas: for area in disallowed_areas: poly = Polygon(numpy.array(area, numpy.float32)) poly = poly.getMinkowskiHull( Polygon( numpy.array( [[-skirt_size, 0], [-skirt_size * 0.707, skirt_size * 0.707], [0, skirt_size], [skirt_size * 0.707, skirt_size * 0.707], [skirt_size, 0], [skirt_size * 0.707, -skirt_size * 0.707], [0, -skirt_size], [-skirt_size * 0.707, -skirt_size * 0.707]], numpy.float32))) areas.append(poly) if skirt_size > 0: half_machine_width = self._active_instance.getMachineSettingValue( "machine_width") / 2 half_machine_depth = self._active_instance.getMachineSettingValue( "machine_depth") / 2 areas.append( Polygon( numpy.array([[-half_machine_width, -half_machine_depth], [-half_machine_width, half_machine_depth], [ -half_machine_width + skirt_size, half_machine_depth - skirt_size ], [ -half_machine_width + skirt_size, -half_machine_depth + skirt_size ]], numpy.float32))) areas.append( Polygon( numpy.array([[half_machine_width, half_machine_depth], [half_machine_width, -half_machine_depth], [ half_machine_width - skirt_size, -half_machine_depth + skirt_size ], [ half_machine_width - skirt_size, half_machine_depth - skirt_size ]], numpy.float32))) areas.append( Polygon( numpy.array([[-half_machine_width, half_machine_depth], [half_machine_width, half_machine_depth], [ half_machine_width - skirt_size, half_machine_depth - skirt_size ], [ -half_machine_width + skirt_size, half_machine_depth - skirt_size ]], numpy.float32))) areas.append( Polygon( numpy.array([[half_machine_width, -half_machine_depth], [-half_machine_width, -half_machine_depth], [ -half_machine_width + skirt_size, -half_machine_depth + skirt_size ], [ half_machine_width - skirt_size, -half_machine_depth + skirt_size ]], numpy.float32))) self._disallowed_areas = areas def _getSkirtSize(self, profile): skirt_size = 0.0 adhesion_type = profile.getSettingValue("adhesion_type") if adhesion_type == "skirt": skirt_distance = profile.getSettingValue("skirt_gap") skirt_line_count = profile.getSettingValue("skirt_line_count") skirt_size = skirt_distance + ( skirt_line_count * profile.getSettingValue("skirt_line_width")) elif adhesion_type == "brim": skirt_size = profile.getSettingValue("brim_width") elif adhesion_type == "raft": skirt_size = profile.getSettingValue("raft_margin") if profile.getSettingValue("draft_shield_enabled"): skirt_size += profile.getSettingValue("draft_shield_dist") skirt_size += profile.getSettingValue("xy_offset") return skirt_size def _clamp(self, value, min_value, max_value): return max(min(value, max_value), min_value) _skirt_settings = [ "adhesion_type", "skirt_gap", "skirt_line_count", "skirt_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist", "xy_offset" ]
def rebuild(self): if self._width == 0 or self._height == 0 or self._depth == 0: return min_w = -self._width / 2 max_w = self._width / 2 min_h = 0.0 max_h = self._height min_d = -self._depth / 2 max_d = self._depth / 2 mb = MeshBuilder() mb.addLine(Vector(min_w, min_h, min_d), Vector(max_w, min_h, min_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, max_h, min_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, max_h, min_d), Vector(max_w, max_h, min_d), color=self.VolumeOutlineColor) mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, max_h, min_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, max_d), Vector(max_w, min_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, max_d), Vector(min_w, max_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, max_h, max_d), Vector(max_w, max_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(max_w, min_h, max_d), Vector(max_w, max_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, min_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, min_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color=self.VolumeOutlineColor) self.setMeshData(mb.getData()) mb = MeshBuilder() mb.addQuad(Vector(min_w, min_h - 0.2, min_d), Vector(max_w, min_h - 0.2, min_d), Vector(max_w, min_h - 0.2, max_d), Vector(min_w, min_h - 0.2, max_d)) self._grid_mesh = mb.getData() for n in range(0, 6): v = self._grid_mesh.getVertex(n) self._grid_mesh.setVertexUVCoordinates(n, v[0], v[2]) disallowed_area_height = 0.1 disallowed_area_size = 0 if self._disallowed_areas: mb = MeshBuilder() color = Color(0.0, 0.0, 0.0, 0.15) for polygon in self._disallowed_areas: points = polygon.getPoints() first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) previous_point = Vector( self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) for point in points: new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height, self._clamp(point[1], min_d, max_d)) mb.addFace(first, previous_point, new_point, color=color) previous_point = new_point # Find the largest disallowed area to exclude it from the maximum scale bounds size = abs(numpy.max(points[:, 1]) - numpy.min(points[:, 1])) disallowed_area_size = max(size, disallowed_area_size) self._disallowed_area_mesh = mb.getData() else: self._disallowed_area_mesh = None self._aabb = AxisAlignedBox(minimum=Vector(min_w, min_h - 1.0, min_d), maximum=Vector(max_w, max_h, max_d)) skirt_size = 0.0 profile = Application.getInstance().getMachineManager( ).getActiveProfile() if profile: skirt_size = self._getSkirtSize(profile) scale_to_max_bounds = AxisAlignedBox( minimum=Vector(min_w + skirt_size, min_h, min_d + skirt_size + disallowed_area_size), maximum=Vector(max_w - skirt_size, max_h, max_d - skirt_size - disallowed_area_size)) Application.getInstance().getController().getScene( )._maximum_bounds = scale_to_max_bounds
def beginRendering(self): scene = self.getController().getScene() renderer = self.getRenderer() if not self._extruders_model: self._extruders_model = Application.getInstance( ).getExtrudersModel() if not self._theme: self._theme = Application.getInstance().getTheme() if not self._enabled_shader: self._enabled_shader = OpenGL.getInstance().createShaderProgram( Resources.getPath(Resources.Shaders, "overhang.shader")) self._enabled_shader.setUniformValue( "u_overhangColor", Color(*self._theme.getColor("model_overhang").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(*self._theme.getColor("model_unslicable").getRgb())) self._disabled_shader.setUniformValue( "u_diffuseColor2", Color(*self._theme.getColor("model_unslicable_alt").getRgb())) self._disabled_shader.setUniformValue("u_width", 50.0) if not self._non_printing_shader: self._non_printing_shader = OpenGL.getInstance( ).createShaderProgram( Resources.getPath(Resources.Shaders, "transparent_object.shader")) self._non_printing_shader.setUniformValue( "u_diffuseColor", Color(*self._theme.getColor("model_non_printing").getRgb())) self._non_printing_shader.setUniformValue("u_opacity", 0.6) if not self._support_mesh_shader: self._support_mesh_shader = OpenGL.getInstance( ).createShaderProgram( Resources.getPath(Resources.Shaders, "striped.shader")) self._support_mesh_shader.setUniformValue("u_vertical_stripes", True) self._support_mesh_shader.setUniformValue("u_width", 5.0) global_container_stack = Application.getInstance( ).getGlobalContainerStack() if global_container_stack: if Application.getInstance().getPreferences().getValue( "view/show_overhang"): # Make sure the overhang angle is valid before passing it to the shader if self._support_angle is not None and self._support_angle >= 0 and self._support_angle <= 90: self._enabled_shader.setUniformValue( "u_overhangAngle", math.cos(math.radians(90 - self._support_angle))) else: self._enabled_shader.setUniformValue( "u_overhangAngle", math.cos(math.radians(0)) ) #Overhang angle of 0 causes no area at all to be marked as overhang. else: self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0))) for node in DepthFirstIterator(scene.getRoot()): if not node.render(renderer): if node.getMeshData() and node.isVisible( ) and not node.callDecoration("getLayerData"): uniforms = {} shade_factor = 1.0 per_mesh_stack = node.callDecoration("getStack") extruder_index = node.callDecoration( "getActiveExtruderPosition") if extruder_index is None: extruder_index = "0" extruder_index = int(extruder_index) # Use the support extruder instead of the active extruder if this is a support_mesh if per_mesh_stack: if per_mesh_stack.getProperty("support_mesh", "value"): extruder_index = int( global_container_stack. getExtruderPositionValueWithDefault( "support_extruder_nr")) try: material_color = self._extruders_model.getItem( extruder_index)["color"] except KeyError: material_color = self._extruders_model.defaultColors[0] if extruder_index != ExtruderManager.getInstance( ).activeExtruderIndex: # Shade objects that are printed with the non-active extruder 25% darker shade_factor = 0.6 try: # Colors are passed as rgb hex strings (eg "#ffffff"), and the shader needs # an rgba list of floats (eg [1.0, 1.0, 1.0, 1.0]) uniforms["diffuse_color"] = [ shade_factor * int(material_color[1:3], 16) / 255, shade_factor * int(material_color[3:5], 16) / 255, shade_factor * int(material_color[5:7], 16) / 255, 1.0 ] # Color the currently selected face-id. face = Selection.getSelectedFace() uniforms["selected_face"] = ( Selection.getMaxFaceSelectionId() + 1) if not face or node != face[0] else face[1] except ValueError: pass if node.callDecoration("isNonPrintingMesh"): if per_mesh_stack and (per_mesh_stack.getProperty( "infill_mesh", "value") or per_mesh_stack.getProperty( "cutting_mesh", "value")): renderer.queueNode( node, shader=self._non_printing_shader, uniforms=uniforms, transparent=True) else: renderer.queueNode( node, shader=self._non_printing_shader, transparent=True) elif getattr(node, "_outside_buildarea", False): renderer.queueNode(node, shader=self._disabled_shader) elif per_mesh_stack and per_mesh_stack.getProperty( "support_mesh", "value"): # Render support meshes with a vertical stripe that is darker shade_factor = 0.6 uniforms["diffuse_color_2"] = [ uniforms["diffuse_color"][0] * shade_factor, uniforms["diffuse_color"][1] * shade_factor, uniforms["diffuse_color"][2] * shade_factor, 1.0 ] renderer.queueNode(node, shader=self._support_mesh_shader, uniforms=uniforms) else: renderer.queueNode(node, shader=self._enabled_shader, uniforms=uniforms) if node.callDecoration("isGroup") and Selection.isSelected( node): renderer.queueNode(scene.getRoot(), mesh=node.getBoundingBoxMesh(), mode=RenderBatch.RenderMode.LineLoop)
def _onNodeDecoratorsChanged(self, node): self._color = Color(35, 35, 35, 0.5) if not node: return
class BuildVolume(SceneNode): VolumeOutlineColor = Color(12, 169, 227, 255) def __init__(self, parent = None): super().__init__(parent) self._width = 0 self._height = 0 self._depth = 0 self._shader = None self._grid_mesh = None self._grid_shader = None self._disallowed_areas = [] self._disallowed_area_mesh = None self.setCalculateBoundingBox(False) self._volume_aabb = None self._active_container_stack = None Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged) self._onGlobalContainerStackChanged() def setWidth(self, width): if width: self._width = width def setHeight(self, height): if height: self._height = height def setDepth(self, depth): if depth: self._depth = depth def getDisallowedAreas(self): return self._disallowed_areas def setDisallowedAreas(self, areas): self._disallowed_areas = areas def render(self, renderer): if not self.getMeshData(): return True if not self._shader: self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "default.shader")) self._grid_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "grid.shader")) renderer.queueNode(self, mode = RenderBatch.RenderMode.Lines) renderer.queueNode(self, mesh = self._grid_mesh, shader = self._grid_shader, backface_cull = True) if self._disallowed_area_mesh: renderer.queueNode(self, mesh = self._disallowed_area_mesh, shader = self._shader, transparent = True, backface_cull = True, sort = -9) return True ## Recalculates the build volume & disallowed areas. def rebuild(self): if not self._width or not self._height or not self._depth: return min_w = -self._width / 2 max_w = self._width / 2 min_h = 0.0 max_h = self._height min_d = -self._depth / 2 max_d = self._depth / 2 mb = MeshBuilder() mb.addLine(Vector(min_w, min_h, min_d), Vector(max_w, min_h, min_d), color = self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, max_h, min_d), color = self.VolumeOutlineColor) mb.addLine(Vector(min_w, max_h, min_d), Vector(max_w, max_h, min_d), color = self.VolumeOutlineColor) mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, max_h, min_d), color = self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, max_d), Vector(max_w, min_h, max_d), color = self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, max_d), Vector(min_w, max_h, max_d), color = self.VolumeOutlineColor) mb.addLine(Vector(min_w, max_h, max_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor) mb.addLine(Vector(max_w, min_h, max_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, min_h, max_d), color = self.VolumeOutlineColor) mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, min_h, max_d), color = self.VolumeOutlineColor) mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color = self.VolumeOutlineColor) mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor) self.setMeshData(mb.build()) mb = MeshBuilder() mb.addQuad( Vector(min_w, min_h - 0.2, min_d), Vector(max_w, min_h - 0.2, min_d), Vector(max_w, min_h - 0.2, max_d), Vector(min_w, min_h - 0.2, max_d) ) for n in range(0, 6): v = mb.getVertex(n) mb.setVertexUVCoordinates(n, v[0], v[2]) self._grid_mesh = mb.build() disallowed_area_height = 0.1 disallowed_area_size = 0 if self._disallowed_areas: mb = MeshBuilder() color = Color(0.0, 0.0, 0.0, 0.15) for polygon in self._disallowed_areas: points = polygon.getPoints() first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) for point in points: new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height, self._clamp(point[1], min_d, max_d)) mb.addFace(first, previous_point, new_point, color = color) previous_point = new_point # Find the largest disallowed area to exclude it from the maximum scale bounds. # This is a very nasty hack. This pretty much only works for UM machines. # This disallowed area_size needs a -lot- of rework at some point in the future: TODO if numpy.min(points[:, 1]) >= 0: # This filters out all areas that have points to the left of the centre. This is done to filter the skirt area. size = abs(numpy.max(points[:, 1]) - numpy.min(points[:, 1])) else: size = 0 disallowed_area_size = max(size, disallowed_area_size) self._disallowed_area_mesh = mb.build() else: self._disallowed_area_mesh = None self._volume_aabb = AxisAlignedBox(minimum = Vector(min_w, min_h - 1.0, min_d), maximum = Vector(max_w, max_h, max_d)) skirt_size = 0.0 container_stack = Application.getInstance().getGlobalContainerStack() if container_stack: skirt_size = self._getSkirtSize(container_stack) # As this works better for UM machines, we only add the disallowed_area_size for the z direction. # This is probably wrong in all other cases. TODO! # The +1 and -1 is added as there is always a bit of extra room required to work properly. scale_to_max_bounds = AxisAlignedBox( minimum = Vector(min_w + skirt_size + 1, min_h, min_d + disallowed_area_size - skirt_size + 1), maximum = Vector(max_w - skirt_size - 1, max_h, max_d - disallowed_area_size + skirt_size - 1) ) Application.getInstance().getController().getScene()._maximum_bounds = scale_to_max_bounds def getBoundingBox(self): return self._volume_aabb def _onGlobalContainerStackChanged(self): if self._active_container_stack: self._active_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged) self._active_container_stack = Application.getInstance().getGlobalContainerStack() if self._active_container_stack: self._active_container_stack.propertyChanged.connect(self._onSettingPropertyChanged) self._width = self._active_container_stack.getProperty("machine_width", "value") if self._active_container_stack.getProperty("print_sequence", "value") == "one_at_a_time": self._height = self._active_container_stack.getProperty("gantry_height", "value") else: self._height = self._active_container_stack.getProperty("machine_height", "value") self._depth = self._active_container_stack.getProperty("machine_depth", "value") self._updateDisallowedAreas() self.rebuild() def _onSettingPropertyChanged(self, setting_key, property_name): if property_name != "value": return if setting_key == "print_sequence": if Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time": self._height = self._active_container_stack.getProperty("gantry_height", "value") else: self._height = self._active_container_stack.getProperty("machine_height", "value") self.rebuild() if setting_key in self._skirt_settings: self._updateDisallowedAreas() self.rebuild() def _updateDisallowedAreas(self): if not self._active_container_stack: return disallowed_areas = self._active_container_stack.getProperty("machine_disallowed_areas", "value") areas = [] skirt_size = self._getSkirtSize(self._active_container_stack) if disallowed_areas: # Extend every area already in the disallowed_areas with the skirt size. for area in disallowed_areas: poly = Polygon(numpy.array(area, numpy.float32)) poly = poly.getMinkowskiHull(Polygon(numpy.array([ [-skirt_size, 0], [-skirt_size * 0.707, skirt_size * 0.707], [0, skirt_size], [skirt_size * 0.707, skirt_size * 0.707], [skirt_size, 0], [skirt_size * 0.707, -skirt_size * 0.707], [0, -skirt_size], [-skirt_size * 0.707, -skirt_size * 0.707] ], numpy.float32))) areas.append(poly) # Add the skirt areas around the borders of the build plate. if skirt_size > 0: half_machine_width = self._active_container_stack.getProperty("machine_width", "value") / 2 half_machine_depth = self._active_container_stack.getProperty("machine_depth", "value") / 2 areas.append(Polygon(numpy.array([ [-half_machine_width, -half_machine_depth], [-half_machine_width, half_machine_depth], [-half_machine_width + skirt_size, half_machine_depth - skirt_size], [-half_machine_width + skirt_size, -half_machine_depth + skirt_size] ], numpy.float32))) areas.append(Polygon(numpy.array([ [half_machine_width, half_machine_depth], [half_machine_width, -half_machine_depth], [half_machine_width - skirt_size, -half_machine_depth + skirt_size], [half_machine_width - skirt_size, half_machine_depth - skirt_size] ], numpy.float32))) areas.append(Polygon(numpy.array([ [-half_machine_width, half_machine_depth], [half_machine_width, half_machine_depth], [half_machine_width - skirt_size, half_machine_depth - skirt_size], [-half_machine_width + skirt_size, half_machine_depth - skirt_size] ], numpy.float32))) areas.append(Polygon(numpy.array([ [half_machine_width, -half_machine_depth], [-half_machine_width, -half_machine_depth], [-half_machine_width + skirt_size, -half_machine_depth + skirt_size], [half_machine_width - skirt_size, -half_machine_depth + skirt_size] ], numpy.float32))) self._disallowed_areas = areas ## Convenience function to calculate the size of the bed adhesion. def _getSkirtSize(self, container_stack): skirt_size = 0.0 adhesion_type = container_stack.getProperty("adhesion_type", "value") if adhesion_type == "skirt": skirt_distance = container_stack.getProperty("skirt_gap", "value") skirt_line_count = container_stack.getProperty("skirt_line_count", "value") skirt_size = skirt_distance + (skirt_line_count * container_stack.getProperty("skirt_line_width", "value")) elif adhesion_type == "brim": skirt_size = container_stack.getProperty("brim_line_count", "value") * container_stack.getProperty("skirt_line_width", "value") elif adhesion_type == "raft": skirt_size = container_stack.getProperty("raft_margin", "value") if container_stack.getProperty("draft_shield_enabled", "value"): skirt_size += container_stack.getProperty("draft_shield_dist", "value") if container_stack.getProperty("xy_offset", "value"): skirt_size += container_stack.getProperty("xy_offset", "value") return skirt_size def _clamp(self, value, min_value, max_value): return max(min(value, max_value), min_value) _skirt_settings = ["adhesion_type", "skirt_gap", "skirt_line_count", "skirt_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist", "xy_offset"]
def event(self, event): modifiers = QApplication.keyboardModifiers() ctrl_is_active = modifiers & Qt.ControlModifier shift_is_active = modifiers & Qt.ShiftModifier if event.type == Event.KeyPressEvent and ctrl_is_active: amount = 10 if shift_is_active else 1 if event.key == KeyEvent.UpKey: self.setLayer(self._current_layer_num + amount) return True if event.key == KeyEvent.DownKey: self.setLayer(self._current_layer_num - amount) return True if event.type == Event.ViewActivateEvent: # Make sure the SimulationPass is created layer_pass = self.getSimulationPass() self.getRenderer().addRenderPass(layer_pass) # Make sure the NozzleNode is add to the root nozzle = self.getNozzleNode() nozzle.setParent(self.getController().getScene().getRoot()) nozzle.setVisible(False) Application.getInstance().globalContainerStackChanged.connect( self._onGlobalStackChanged) self._onGlobalStackChanged() if not self._simulationview_composite_shader: self._simulationview_composite_shader = OpenGL.getInstance( ).createShaderProgram( os.path.join( PluginRegistry.getInstance().getPluginPath( "SimulationView"), "simulationview_composite.shader")) theme = Application.getInstance().getTheme() self._simulationview_composite_shader.setUniformValue( "u_background_color", Color(*theme.getColor("viewport_background").getRgb())) self._simulationview_composite_shader.setUniformValue( "u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb())) if not self._composite_pass: self._composite_pass = self.getRenderer().getRenderPass( "composite") self._old_layer_bindings = self._composite_pass.getLayerBindings( )[:] # make a copy so we can restore to it later self._composite_pass.getLayerBindings().append("simulationview") self._old_composite_shader = self._composite_pass.getCompositeShader( ) self._composite_pass.setCompositeShader( self._simulationview_composite_shader) elif event.type == Event.ViewDeactivateEvent: self._wireprint_warning_message.hide() Application.getInstance().globalContainerStackChanged.disconnect( self._onGlobalStackChanged) if self._global_container_stack: self._global_container_stack.propertyChanged.disconnect( self._onPropertyChanged) self._nozzle_node.setParent(None) self.getRenderer().removeRenderPass(self._layer_pass) self._composite_pass.setLayerBindings(self._old_layer_bindings) self._composite_pass.setCompositeShader(self._old_composite_shader)
class BuildVolume(SceneNode): VolumeOutlineColor = Color(12, 169, 227, 255) XAxisColor = Color(255, 0, 0, 255) YAxisColor = Color(0, 0, 255, 255) ZAxisColor = Color(0, 255, 0, 255) raftThicknessChanged = Signal() def __init__(self, parent=None): super().__init__(parent) self._width = 0 self._height = 0 self._depth = 0 self._shader = None self._origin_mesh = None self._origin_line_length = 20 self._origin_line_width = 0.5 self._grid_mesh = None self._grid_shader = None self._disallowed_areas = [] self._disallowed_area_mesh = None self._error_areas = [] self._error_mesh = None self.setCalculateBoundingBox(False) self._volume_aabb = None self._raft_thickness = 0.0 self._adhesion_type = None self._platform = Platform(self) self._global_container_stack = None Application.getInstance().globalContainerStackChanged.connect( self._onStackChanged) self._onStackChanged() self._has_errors = False Application.getInstance().getController().getScene( ).sceneChanged.connect(self._onSceneChanged) #Objects loaded at the moment. We are connected to the property changed events of these objects. self._scene_objects = set() self._change_timer = QTimer() self._change_timer.setInterval(100) self._change_timer.setSingleShot(True) self._change_timer.timeout.connect(self._onChangeTimerFinished) self._build_volume_message = Message( catalog.i18nc( "@info:status", "The build volume height has been reduced due to the value of the" " \"Print Sequence\" setting to prevent the gantry from colliding" " with printed models.")) # Must be after setting _build_volume_message, apparently that is used in getMachineManager. # activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality. # Therefore this works. Application.getInstance().getMachineManager( ).activeQualityChanged.connect(self._onStackChanged) # This should also ways work, and it is semantically more correct, # but it does not update the disallowed areas after material change Application.getInstance().getMachineManager( ).activeStackChanged.connect(self._onStackChanged) def _onSceneChanged(self, source): if self._global_container_stack: self._change_timer.start() def _onChangeTimerFinished(self): root = Application.getInstance().getController().getScene().getRoot() new_scene_objects = set( node for node in BreadthFirstIterator(root) if node.getMeshData() and type(node) is SceneNode) if new_scene_objects != self._scene_objects: for node in new_scene_objects - self._scene_objects: #Nodes that were added to the scene. node.decoratorsChanged.connect(self._onNodeDecoratorChanged) for node in self._scene_objects - new_scene_objects: #Nodes that were removed from the scene. per_mesh_stack = node.callDecoration("getStack") if per_mesh_stack: per_mesh_stack.propertyChanged.disconnect( self._onSettingPropertyChanged) active_extruder_changed = node.callDecoration( "getActiveExtruderChangedSignal") if active_extruder_changed is not None: node.callDecoration( "getActiveExtruderChangedSignal").disconnect( self._updateDisallowedAreasAndRebuild) node.decoratorsChanged.disconnect(self._onNodeDecoratorChanged) self._scene_objects = new_scene_objects self._onSettingPropertyChanged( "print_sequence", "value") # Create fake event, so right settings are triggered. ## Updates the listeners that listen for changes in per-mesh stacks. # # \param node The node for which the decorators changed. def _onNodeDecoratorChanged(self, node): per_mesh_stack = node.callDecoration("getStack") if per_mesh_stack: per_mesh_stack.propertyChanged.connect( self._onSettingPropertyChanged) active_extruder_changed = node.callDecoration( "getActiveExtruderChangedSignal") if active_extruder_changed is not None: active_extruder_changed.connect( self._updateDisallowedAreasAndRebuild) self._updateDisallowedAreasAndRebuild() def setWidth(self, width): if width: self._width = width def setHeight(self, height): if height: self._height = height def setDepth(self, depth): if depth: self._depth = depth def getDisallowedAreas(self): return self._disallowed_areas def setDisallowedAreas(self, areas): self._disallowed_areas = areas def render(self, renderer): if not self.getMeshData(): return True if not self._shader: self._shader = OpenGL.getInstance().createShaderProgram( Resources.getPath(Resources.Shaders, "default.shader")) self._grid_shader = OpenGL.getInstance().createShaderProgram( Resources.getPath(Resources.Shaders, "grid.shader")) renderer.queueNode(self, mode=RenderBatch.RenderMode.Lines) renderer.queueNode(self, mesh=self._origin_mesh) renderer.queueNode(self, mesh=self._grid_mesh, shader=self._grid_shader, backface_cull=True) if self._disallowed_area_mesh: renderer.queueNode(self, mesh=self._disallowed_area_mesh, shader=self._shader, transparent=True, backface_cull=True, sort=-9) if self._error_mesh: renderer.queueNode(self, mesh=self._error_mesh, shader=self._shader, transparent=True, backface_cull=True, sort=-8) return True ## Recalculates the build volume & disallowed areas. def rebuild(self): if not self._width or not self._height or not self._depth: return min_w = -self._width / 2 max_w = self._width / 2 min_h = 0.0 max_h = self._height min_d = -self._depth / 2 max_d = self._depth / 2 mb = MeshBuilder() # Outline 'cube' of the build volume mb.addLine(Vector(min_w, min_h, min_d), Vector(max_w, min_h, min_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, max_h, min_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, max_h, min_d), Vector(max_w, max_h, min_d), color=self.VolumeOutlineColor) mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, max_h, min_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, max_d), Vector(max_w, min_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, max_d), Vector(min_w, max_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, max_h, max_d), Vector(max_w, max_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(max_w, min_h, max_d), Vector(max_w, max_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, min_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, min_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color=self.VolumeOutlineColor) self.setMeshData(mb.build()) mb = MeshBuilder() # Indication of the machine origin if self._global_container_stack.getProperty("machine_center_is_zero", "value"): origin = (Vector(min_w, min_h, min_d) + Vector(max_w, min_h, max_d)) / 2 else: origin = Vector(min_w, min_h, max_d) mb.addCube(width=self._origin_line_length, height=self._origin_line_width, depth=self._origin_line_width, center=origin + Vector(self._origin_line_length / 2, 0, 0), color=self.XAxisColor) mb.addCube(width=self._origin_line_width, height=self._origin_line_length, depth=self._origin_line_width, center=origin + Vector(0, self._origin_line_length / 2, 0), color=self.YAxisColor) mb.addCube(width=self._origin_line_width, height=self._origin_line_width, depth=self._origin_line_length, center=origin - Vector(0, 0, self._origin_line_length / 2), color=self.ZAxisColor) self._origin_mesh = mb.build() mb = MeshBuilder() mb.addQuad(Vector(min_w, min_h - 0.2, min_d), Vector(max_w, min_h - 0.2, min_d), Vector(max_w, min_h - 0.2, max_d), Vector(min_w, min_h - 0.2, max_d)) for n in range(0, 6): v = mb.getVertex(n) mb.setVertexUVCoordinates(n, v[0], v[2]) self._grid_mesh = mb.build() disallowed_area_height = 0.1 disallowed_area_size = 0 if self._disallowed_areas: mb = MeshBuilder() color = Color(0.0, 0.0, 0.0, 0.15) for polygon in self._disallowed_areas: points = polygon.getPoints() first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) previous_point = Vector( self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) for point in points: new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height, self._clamp(point[1], min_d, max_d)) mb.addFace(first, previous_point, new_point, color=color) previous_point = new_point # Find the largest disallowed area to exclude it from the maximum scale bounds. # This is a very nasty hack. This pretty much only works for UM machines. # This disallowed area_size needs a -lot- of rework at some point in the future: TODO if numpy.min( points[:, 1] ) >= 0: # This filters out all areas that have points to the left of the centre. This is done to filter the skirt area. size = abs( numpy.max(points[:, 1]) - numpy.min(points[:, 1])) else: size = 0 disallowed_area_size = max(size, disallowed_area_size) self._disallowed_area_mesh = mb.build() else: self._disallowed_area_mesh = None if self._error_areas: mb = MeshBuilder() for error_area in self._error_areas: color = Color(1.0, 0.0, 0.0, 0.5) points = error_area.getPoints() first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) previous_point = Vector( self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) for point in points: new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height, self._clamp(point[1], min_d, max_d)) mb.addFace(first, previous_point, new_point, color=color) previous_point = new_point self._error_mesh = mb.build() else: self._error_mesh = None self._volume_aabb = AxisAlignedBox( minimum=Vector(min_w, min_h - 1.0, min_d), maximum=Vector(max_w, max_h - self._raft_thickness, max_d)) bed_adhesion_size = self._getEdgeDisallowedSize() # As this works better for UM machines, we only add the disallowed_area_size for the z direction. # This is probably wrong in all other cases. TODO! # The +1 and -1 is added as there is always a bit of extra room required to work properly. scale_to_max_bounds = AxisAlignedBox( minimum=Vector( min_w + bed_adhesion_size + 1, min_h, min_d + disallowed_area_size - bed_adhesion_size + 1), maximum=Vector( max_w - bed_adhesion_size - 1, max_h - self._raft_thickness, max_d - disallowed_area_size + bed_adhesion_size - 1)) Application.getInstance().getController().getScene( )._maximum_bounds = scale_to_max_bounds def getBoundingBox(self): return self._volume_aabb def getRaftThickness(self): return self._raft_thickness def _updateRaftThickness(self): old_raft_thickness = self._raft_thickness self._adhesion_type = self._global_container_stack.getProperty( "adhesion_type", "value") self._raft_thickness = 0.0 if self._adhesion_type == "raft": self._raft_thickness = (self._global_container_stack.getProperty( "raft_base_thickness", "value") + self._global_container_stack.getProperty( "raft_interface_thickness", "value") + self._global_container_stack.getProperty( "raft_surface_layers", "value") * self._global_container_stack.getProperty( "raft_surface_thickness", "value") + self._global_container_stack.getProperty( "raft_airgap", "value")) # Rounding errors do not matter, we check if raft_thickness has changed at all if old_raft_thickness != self._raft_thickness: self.setPosition(Vector(0, -self._raft_thickness, 0), SceneNode.TransformSpace.World) self.raftThicknessChanged.emit() ## Update the build volume visualization def _onStackChanged(self): if self._global_container_stack: self._global_container_stack.propertyChanged.disconnect( self._onSettingPropertyChanged) extruders = ExtruderManager.getInstance().getMachineExtruders( self._global_container_stack.getId()) for extruder in extruders: extruder.propertyChanged.disconnect( self._onSettingPropertyChanged) self._global_container_stack = Application.getInstance( ).getGlobalContainerStack() if self._global_container_stack: self._global_container_stack.propertyChanged.connect( self._onSettingPropertyChanged) extruders = ExtruderManager.getInstance().getMachineExtruders( self._global_container_stack.getId()) for extruder in extruders: extruder.propertyChanged.connect( self._onSettingPropertyChanged) self._width = self._global_container_stack.getProperty( "machine_width", "value") machine_height = self._global_container_stack.getProperty( "machine_height", "value") if self._global_container_stack.getProperty( "print_sequence", "value") == "one_at_a_time" and len( self._scene_objects) > 1: self._height = min( self._global_container_stack.getProperty( "gantry_height", "value"), machine_height) if self._height < machine_height: self._build_volume_message.show() else: self._build_volume_message.hide() else: self._height = self._global_container_stack.getProperty( "machine_height", "value") self._build_volume_message.hide() self._depth = self._global_container_stack.getProperty( "machine_depth", "value") self._updateDisallowedAreas() self._updateRaftThickness() self.rebuild() def _onSettingPropertyChanged(self, setting_key, property_name): if property_name != "value": return rebuild_me = False if setting_key == "print_sequence": machine_height = self._global_container_stack.getProperty( "machine_height", "value") if Application.getInstance().getGlobalContainerStack().getProperty( "print_sequence", "value") == "one_at_a_time" and len( self._scene_objects) > 1: self._height = min( self._global_container_stack.getProperty( "gantry_height", "value"), machine_height) if self._height < machine_height: self._build_volume_message.show() else: self._build_volume_message.hide() else: self._height = self._global_container_stack.getProperty( "machine_height", "value") self._build_volume_message.hide() rebuild_me = True if setting_key in self._skirt_settings or setting_key in self._prime_settings or setting_key in self._tower_settings or setting_key == "print_sequence" or setting_key in self._ooze_shield_settings or setting_key in self._distance_settings or setting_key in self._extruder_settings: self._updateDisallowedAreas() rebuild_me = True if setting_key in self._raft_settings: self._updateRaftThickness() rebuild_me = True if rebuild_me: self.rebuild() def hasErrors(self): return self._has_errors ## Calls _updateDisallowedAreas and makes sure the changes appear in the # scene. # # This is required for a signal to trigger the update in one go. The # ``_updateDisallowedAreas`` method itself shouldn't call ``rebuild``, # since there may be other changes before it needs to be rebuilt, which # would hit performance. def _updateDisallowedAreasAndRebuild(self): self._updateDisallowedAreas() self.rebuild() def _updateDisallowedAreas(self): if not self._global_container_stack: return self._error_areas = [] extruder_manager = ExtruderManager.getInstance() used_extruders = extruder_manager.getUsedExtruderStacks() disallowed_border_size = self._getEdgeDisallowedSize() result_areas = self._computeDisallowedAreasStatic( disallowed_border_size, used_extruders ) #Normal machine disallowed areas can always be added. prime_areas = self._computeDisallowedAreasPrime( disallowed_border_size, used_extruders) prime_disallowed_areas = self._computeDisallowedAreasStatic( 0, used_extruders ) #Where the priming is not allowed to happen. This is not added to the result, just for collision checking. #Check if prime positions intersect with disallowed areas. for extruder in used_extruders: extruder_id = extruder.getId() collision = False for prime_polygon in prime_areas[extruder_id]: for disallowed_polygon in prime_disallowed_areas[extruder_id]: if prime_polygon.intersectsPolygon( disallowed_polygon) is not None: collision = True break if collision: break #Also check other prime positions (without additional offset). for other_extruder_id in prime_areas: if extruder_id == other_extruder_id: #It is allowed to collide with itself. continue for other_prime_polygon in prime_areas[other_extruder_id]: if prime_polygon.intersectsPolygon( other_prime_polygon): collision = True break if collision: break if collision: break if not collision: #Prime areas are valid. Add as normal. result_areas[extruder_id].extend(prime_areas[extruder_id]) nozzle_disallowed_areas = extruder.getProperty( "nozzle_disallowed_areas", "value") for area in nozzle_disallowed_areas: polygon = Polygon(numpy.array(area, numpy.float32)) polygon = polygon.getMinkowskiHull( Polygon.approximatedCircle(disallowed_border_size)) result_areas[extruder_id].append( polygon) #Don't perform the offset on these. # Add prime tower location as disallowed area. prime_tower_collision = False prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders) for extruder_id in prime_tower_areas: for prime_tower_area in prime_tower_areas[extruder_id]: for area in result_areas[extruder_id]: if prime_tower_area.intersectsPolygon(area) is not None: prime_tower_collision = True break if prime_tower_collision: #Already found a collision. break if not prime_tower_collision: result_areas[extruder_id].extend( prime_tower_areas[extruder_id]) else: self._error_areas.extend(prime_tower_areas[extruder_id]) self._has_errors = len(self._error_areas) > 0 self._disallowed_areas = [] for extruder_id in result_areas: self._disallowed_areas.extend(result_areas[extruder_id]) ## Computes the disallowed areas for objects that are printed with print # features. # # This means that the brim, travel avoidance and such will be applied to # these features. # # \return A dictionary with for each used extruder ID the disallowed areas # where that extruder may not print. def _computeDisallowedAreasPrinted(self, used_extruders): result = {} for extruder in used_extruders: result[extruder.getId()] = [] #Currently, the only normally printed object is the prime tower. if ExtruderManager.getInstance().getResolveOrValue( "prime_tower_enable") == True: prime_tower_size = self._global_container_stack.getProperty( "prime_tower_size", "value") machine_width = self._global_container_stack.getProperty( "machine_width", "value") machine_depth = self._global_container_stack.getProperty( "machine_depth", "value") prime_tower_x = self._global_container_stack.getProperty( "prime_tower_position_x", "value" ) - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left. prime_tower_y = -self._global_container_stack.getProperty( "prime_tower_position_y", "value") + machine_depth / 2 prime_tower_area = Polygon([ [ prime_tower_x - prime_tower_size, prime_tower_y - prime_tower_size ], [prime_tower_x, prime_tower_y - prime_tower_size], [prime_tower_x, prime_tower_y], [prime_tower_x - prime_tower_size, prime_tower_y], ]) prime_tower_area = prime_tower_area.getMinkowskiHull( Polygon.approximatedCircle(0)) for extruder in used_extruders: result[extruder.getId()].append( prime_tower_area ) #The prime tower location is the same for each extruder, regardless of offset. return result ## Computes the disallowed areas for the prime locations. # # These are special because they are not subject to things like brim or # travel avoidance. They do get a dilute with the border size though # because they may not intersect with brims and such of other objects. # # \param border_size The size with which to offset the disallowed areas # due to skirt, brim, travel avoid distance, etc. # \param used_extruders The extruder stacks to generate disallowed areas # for. # \return A dictionary with for each used extruder ID the prime areas. def _computeDisallowedAreasPrime(self, border_size, used_extruders): result = {} machine_width = self._global_container_stack.getProperty( "machine_width", "value") machine_depth = self._global_container_stack.getProperty( "machine_depth", "value") for extruder in used_extruders: prime_x = extruder.getProperty( "extruder_prime_pos_x", "value" ) - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left. prime_y = machine_depth / 2 - extruder.getProperty( "extruder_prime_pos_y", "value") prime_polygon = Polygon.approximatedCircle(PRIME_CLEARANCE) prime_polygon = prime_polygon.translate(prime_x, prime_y) prime_polygon = prime_polygon.getMinkowskiHull( Polygon.approximatedCircle(border_size)) result[extruder.getId()] = [prime_polygon] return result ## Computes the disallowed areas that are statically placed in the machine. # # It computes different disallowed areas depending on the offset of the # extruder. The resulting dictionary will therefore have an entry for each # extruder that is used. # # \param border_size The size with which to offset the disallowed areas # due to skirt, brim, travel avoid distance, etc. # \param used_extruders The extruder stacks to generate disallowed areas # for. # \return A dictionary with for each used extruder ID the disallowed areas # where that extruder may not print. def _computeDisallowedAreasStatic(self, border_size, used_extruders): #Convert disallowed areas to polygons and dilate them. machine_disallowed_polygons = [] for area in self._global_container_stack.getProperty( "machine_disallowed_areas", "value"): polygon = Polygon(numpy.array(area, numpy.float32)) polygon = polygon.getMinkowskiHull( Polygon.approximatedCircle(border_size)) machine_disallowed_polygons.append(polygon) result = {} for extruder in used_extruders: extruder_id = extruder.getId() offset_x = extruder.getProperty("machine_nozzle_offset_x", "value") if offset_x is None: offset_x = 0 offset_y = extruder.getProperty("machine_nozzle_offset_y", "value") if offset_y is None: offset_y = 0 result[extruder_id] = [] for polygon in machine_disallowed_polygons: result[extruder_id].append( polygon.translate(offset_x, offset_y) ) #Compensate for the nozzle offset of this extruder. #Add the border around the edge of the build volume. left_unreachable_border = 0 right_unreachable_border = 0 top_unreachable_border = 0 bottom_unreachable_border = 0 #The build volume is defined as the union of the area that all extruders can reach, so we need to know the relative offset to all extruders. for other_extruder in ExtruderManager.getInstance( ).getActiveExtruderStacks(): other_offset_x = other_extruder.getProperty( "machine_nozzle_offset_x", "value") other_offset_y = other_extruder.getProperty( "machine_nozzle_offset_y", "value") left_unreachable_border = min(left_unreachable_border, other_offset_x - offset_x) right_unreachable_border = max(right_unreachable_border, other_offset_x - offset_x) top_unreachable_border = min(top_unreachable_border, other_offset_y - offset_y) bottom_unreachable_border = max(bottom_unreachable_border, other_offset_y - offset_y) half_machine_width = self._global_container_stack.getProperty( "machine_width", "value") / 2 half_machine_depth = self._global_container_stack.getProperty( "machine_depth", "value") / 2 if border_size - left_unreachable_border > 0: result[extruder_id].append( Polygon( numpy.array( [[-half_machine_width, -half_machine_depth], [-half_machine_width, half_machine_depth], [ -half_machine_width + border_size - left_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border ], [ -half_machine_width + border_size - left_unreachable_border, -half_machine_depth + border_size - top_unreachable_border ]], numpy.float32))) if border_size + right_unreachable_border > 0: result[extruder_id].append( Polygon( numpy.array( [[half_machine_width, half_machine_depth], [half_machine_width, -half_machine_depth], [ half_machine_width - border_size - right_unreachable_border, -half_machine_depth + border_size - top_unreachable_border ], [ half_machine_width - border_size - right_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border ]], numpy.float32))) if border_size + bottom_unreachable_border > 0: result[extruder_id].append( Polygon( numpy.array( [[-half_machine_width, half_machine_depth], [half_machine_width, half_machine_depth], [ half_machine_width - border_size - right_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border ], [ -half_machine_width + border_size - left_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border ]], numpy.float32))) if border_size - top_unreachable_border > 0: result[extruder_id].append( Polygon( numpy.array( [[half_machine_width, -half_machine_depth], [-half_machine_width, -half_machine_depth], [ -half_machine_width + border_size - left_unreachable_border, -half_machine_depth + border_size - top_unreachable_border ], [ half_machine_width - border_size - right_unreachable_border, -half_machine_depth + border_size - top_unreachable_border ]], numpy.float32))) return result ## Private convenience function to get a setting from the adhesion # extruder. # # \param setting_key The key of the setting to get. # \param property The property to get from the setting. # \return The property of the specified setting in the adhesion extruder. def _getSettingFromAdhesionExtruder(self, setting_key, property="value"): return self._getSettingFromExtruder(setting_key, "adhesion_extruder_nr", property) ## Private convenience function to get a setting from every extruder. # # For single extrusion machines, this gets the setting from the global # stack. # # \return A sequence of setting values, one for each extruder. def _getSettingFromAllExtruders(self, setting_key, property="value"): return ExtruderManager.getInstance().getAllExtruderSettings( setting_key, property) ## Private convenience function to get a setting from the support infill # extruder. # # \param setting_key The key of the setting to get. # \param property The property to get from the setting. # \return The property of the specified setting in the support infill # extruder. def _getSettingFromSupportInfillExtruder(self, setting_key, property="value"): return self._getSettingFromExtruder(setting_key, "support_infill_extruder_nr", property) ## Helper function to get a setting from an extruder specified in another # setting. # # \param setting_key The key of the setting to get. # \param extruder_setting_key The key of the setting that specifies from # which extruder to get the setting, if there are multiple extruders. # \param property The property to get from the setting. # \return The property of the specified setting in the specified extruder. def _getSettingFromExtruder(self, setting_key, extruder_setting_key, property="value"): multi_extrusion = self._global_container_stack.getProperty( "machine_extruder_count", "value") > 1 if not multi_extrusion: return self._global_container_stack.getProperty( setting_key, property) extruder_index = self._global_container_stack.getProperty( extruder_setting_key, "value") if extruder_index == "-1": # If extruder index is -1 use global instead return self._global_container_stack.getProperty( setting_key, property) extruder_stack_id = ExtruderManager.getInstance().extruderIds[str( extruder_index)] stack = UM.Settings.ContainerRegistry.getInstance( ).findContainerStacks(id=extruder_stack_id)[0] return stack.getProperty(setting_key, property) ## Convenience function to calculate the disallowed radius around the edge. # # This disallowed radius is to allow for space around the models that is # not part of the collision radius, such as bed adhesion (skirt/brim/raft) # and travel avoid distance. def _getEdgeDisallowedSize(self): if not self._global_container_stack: return 0 container_stack = self._global_container_stack # If we are printing one at a time, we need to add the bed adhesion size to the disallowed areas of the objects if container_stack.getProperty("print_sequence", "value") == "one_at_a_time": return 0.1 # Return a very small value, so we do draw disallowed area's near the edges. adhesion_type = container_stack.getProperty("adhesion_type", "value") if adhesion_type == "skirt": skirt_distance = self._getSettingFromAdhesionExtruder("skirt_gap") skirt_line_count = self._getSettingFromAdhesionExtruder( "skirt_line_count") bed_adhesion_size = skirt_distance + ( skirt_line_count * self._getSettingFromAdhesionExtruder("skirt_brim_line_width")) if self._global_container_stack.getProperty( "machine_extruder_count", "value") > 1: adhesion_extruder_nr = int( self._global_container_stack.getProperty( "adhesion_extruder_nr", "value")) extruder_values = ExtruderManager.getInstance( ).getAllExtruderValues("skirt_brim_line_width") del extruder_values[ adhesion_extruder_nr] # Remove the value of the adhesion extruder nr. for value in extruder_values: bed_adhesion_size += value elif adhesion_type == "brim": bed_adhesion_size = self._getSettingFromAdhesionExtruder( "brim_line_count") * self._getSettingFromAdhesionExtruder( "skirt_brim_line_width") if self._global_container_stack.getProperty( "machine_extruder_count", "value") > 1: adhesion_extruder_nr = int( self._global_container_stack.getProperty( "adhesion_extruder_nr", "value")) extruder_values = ExtruderManager.getInstance( ).getAllExtruderValues("skirt_brim_line_width") del extruder_values[ adhesion_extruder_nr] # Remove the value of the adhesion extruder nr. for value in extruder_values: bed_adhesion_size += value elif adhesion_type == "raft": bed_adhesion_size = self._getSettingFromAdhesionExtruder( "raft_margin") elif adhesion_type == "none": bed_adhesion_size = 0 else: raise Exception( "Unknown bed adhesion type. Did you forget to update the build volume calculations for your new bed adhesion type?" ) support_expansion = 0 if self._getSettingFromSupportInfillExtruder( "support_offset") and self._global_container_stack.getProperty( "support_enable", "value"): support_expansion += self._getSettingFromSupportInfillExtruder( "support_offset") farthest_shield_distance = 0 if container_stack.getProperty("draft_shield_enabled", "value"): farthest_shield_distance = max( farthest_shield_distance, container_stack.getProperty("draft_shield_dist", "value")) if container_stack.getProperty("ooze_shield_enabled", "value"): farthest_shield_distance = max( farthest_shield_distance, container_stack.getProperty("ooze_shield_dist", "value")) move_from_wall_radius = 0 # Moves that start from outer wall. move_from_wall_radius = max( move_from_wall_radius, max(self._getSettingFromAllExtruders("infill_wipe_dist"))) avoid_enabled_per_extruder = self._getSettingFromAllExtruders( ("travel_avoid_other_parts")) avoid_distance_per_extruder = self._getSettingFromAllExtruders( "travel_avoid_distance") for index, avoid_other_parts_enabled in enumerate( avoid_enabled_per_extruder ): #For each extruder (or just global). if avoid_other_parts_enabled: move_from_wall_radius = max(move_from_wall_radius, avoid_distance_per_extruder[index] ) #Index of the same extruder. #Now combine our different pieces of data to get the final border size. #Support expansion is added to the bed adhesion, since the bed adhesion goes around support. #Support expansion is added to farthest shield distance, since the shields go around support. border_size = max(move_from_wall_radius, support_expansion + farthest_shield_distance, support_expansion + bed_adhesion_size) return border_size def _clamp(self, value, min_value, max_value): return max(min(value, max_value), min_value) _skirt_settings = [ "adhesion_type", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist" ] _raft_settings = [ "adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap" ] _prime_settings = [ "extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z" ] _tower_settings = [ "prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y" ] _ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"] _distance_settings = [ "infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts" ] _extruder_settings = [ "support_enable", "support_interface_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_interface_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type" ] #Settings that can affect which extruders are used.
def rebuild(self): if not self._width or not self._height or not self._depth: return if not Application.getInstance()._engine: return if not self._volume_outline_color: theme = Application.getInstance().getTheme() self._volume_outline_color = Color(*theme.getColor("volume_outline").getRgb()) self._x_axis_color = Color(*theme.getColor("x_axis").getRgb()) self._y_axis_color = Color(*theme.getColor("y_axis").getRgb()) self._z_axis_color = Color(*theme.getColor("z_axis").getRgb()) self._disallowed_area_color = Color(*theme.getColor("disallowed_area").getRgb()) self._error_area_color = Color(*theme.getColor("error_area").getRgb()) min_w = -self._width / 2 max_w = self._width / 2 min_h = 0.0 max_h = self._height min_d = -self._depth / 2 max_d = self._depth / 2 z_fight_distance = 0.2 # Distance between buildplate and disallowed area meshes to prevent z-fighting if self._shape != "elliptic": # Outline 'cube' of the build volume mb = MeshBuilder() mb.addLine(Vector(min_w, min_h, min_d), Vector(max_w, min_h, min_d), color = self._volume_outline_color) mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, max_h, min_d), color = self._volume_outline_color) mb.addLine(Vector(min_w, max_h, min_d), Vector(max_w, max_h, min_d), color = self._volume_outline_color) mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, max_h, min_d), color = self._volume_outline_color) mb.addLine(Vector(min_w, min_h, max_d), Vector(max_w, min_h, max_d), color = self._volume_outline_color) mb.addLine(Vector(min_w, min_h, max_d), Vector(min_w, max_h, max_d), color = self._volume_outline_color) mb.addLine(Vector(min_w, max_h, max_d), Vector(max_w, max_h, max_d), color = self._volume_outline_color) mb.addLine(Vector(max_w, min_h, max_d), Vector(max_w, max_h, max_d), color = self._volume_outline_color) mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, min_h, max_d), color = self._volume_outline_color) mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, min_h, max_d), color = self._volume_outline_color) mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color = self._volume_outline_color) mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color = self._volume_outline_color) self.setMeshData(mb.build()) # Build plate grid mesh mb = MeshBuilder() mb.addQuad( Vector(min_w, min_h - z_fight_distance, min_d), Vector(max_w, min_h - z_fight_distance, min_d), Vector(max_w, min_h - z_fight_distance, max_d), Vector(min_w, min_h - z_fight_distance, max_d) ) for n in range(0, 6): v = mb.getVertex(n) mb.setVertexUVCoordinates(n, v[0], v[2]) self._grid_mesh = mb.build() else: # Bottom and top 'ellipse' of the build volume aspect = 1.0 scale_matrix = Matrix() if self._width != 0: # Scale circular meshes by aspect ratio if width != height aspect = self._depth / self._width scale_matrix.compose(scale = Vector(1, 1, aspect)) mb = MeshBuilder() mb.addArc(max_w, Vector.Unit_Y, center = (0, min_h - z_fight_distance, 0), color = self._volume_outline_color) mb.addArc(max_w, Vector.Unit_Y, center = (0, max_h, 0), color = self._volume_outline_color) self.setMeshData(mb.build().getTransformed(scale_matrix)) # Build plate grid mesh mb = MeshBuilder() mb.addVertex(0, min_h - z_fight_distance, 0) mb.addArc(max_w, Vector.Unit_Y, center = Vector(0, min_h - z_fight_distance, 0)) sections = mb.getVertexCount() - 1 # Center point is not an arc section indices = [] for n in range(0, sections - 1): indices.append([0, n + 2, n + 1]) mb.addIndices(numpy.asarray(indices, dtype = numpy.int32)) mb.calculateNormals() for n in range(0, mb.getVertexCount()): v = mb.getVertex(n) mb.setVertexUVCoordinates(n, v[0], v[2] * aspect) self._grid_mesh = mb.build().getTransformed(scale_matrix) # Indication of the machine origin if self._global_container_stack.getProperty("machine_center_is_zero", "value"): origin = (Vector(min_w, min_h, min_d) + Vector(max_w, min_h, max_d)) / 2 else: origin = Vector(min_w, min_h, max_d) mb = MeshBuilder() mb.addCube( width = self._origin_line_length, height = self._origin_line_width, depth = self._origin_line_width, center = origin + Vector(self._origin_line_length / 2, 0, 0), color = self._x_axis_color ) mb.addCube( width = self._origin_line_width, height = self._origin_line_length, depth = self._origin_line_width, center = origin + Vector(0, self._origin_line_length / 2, 0), color = self._y_axis_color ) mb.addCube( width = self._origin_line_width, height = self._origin_line_width, depth = self._origin_line_length, center = origin - Vector(0, 0, self._origin_line_length / 2), color = self._z_axis_color ) self._origin_mesh = mb.build() disallowed_area_height = 0.1 disallowed_area_size = 0 if self._disallowed_areas: mb = MeshBuilder() color = self._disallowed_area_color for polygon in self._disallowed_areas: points = polygon.getPoints() if len(points) == 0: continue first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) for point in points: new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height, self._clamp(point[1], min_d, max_d)) mb.addFace(first, previous_point, new_point, color = color) previous_point = new_point # Find the largest disallowed area to exclude it from the maximum scale bounds. # This is a very nasty hack. This pretty much only works for UM machines. # This disallowed area_size needs a -lot- of rework at some point in the future: TODO if numpy.min(points[:, 1]) >= 0: # This filters out all areas that have points to the left of the centre. This is done to filter the skirt area. size = abs(numpy.max(points[:, 1]) - numpy.min(points[:, 1])) else: size = 0 disallowed_area_size = max(size, disallowed_area_size) self._disallowed_area_mesh = mb.build() else: self._disallowed_area_mesh = None if self._error_areas: mb = MeshBuilder() for error_area in self._error_areas: color = self._error_area_color points = error_area.getPoints() first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) for point in points: new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height, self._clamp(point[1], min_d, max_d)) mb.addFace(first, previous_point, new_point, color=color) previous_point = new_point self._error_mesh = mb.build() else: self._error_mesh = None self._volume_aabb = AxisAlignedBox( minimum = Vector(min_w, min_h - 1.0, min_d), maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d)) bed_adhesion_size = self._getEdgeDisallowedSize() # As this works better for UM machines, we only add the disallowed_area_size for the z direction. # This is probably wrong in all other cases. TODO! # The +1 and -1 is added as there is always a bit of extra room required to work properly. scale_to_max_bounds = AxisAlignedBox( minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + disallowed_area_size - bed_adhesion_size + 1), maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - disallowed_area_size + bed_adhesion_size - 1) ) Application.getInstance().getController().getScene()._maximum_bounds = scale_to_max_bounds
def _checkSetup(self): if not self._extruders_model: self._extruders_model = Application.getInstance().getExtrudersModel() if not self._theme: self._theme = Application.getInstance().getTheme() if not self._enabled_shader: self._enabled_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "overhang.shader")) self._enabled_shader.setUniformValue("u_overhangColor", Color(*self._theme.getColor("model_overhang").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(*self._theme.getColor("model_unslicable").getRgb())) self._disabled_shader.setUniformValue("u_diffuseColor2", Color(*self._theme.getColor("model_unslicable_alt").getRgb())) self._disabled_shader.setUniformValue("u_width", 50.0) if not self._non_printing_shader: self._non_printing_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "transparent_object.shader")) self._non_printing_shader.setUniformValue("u_diffuseColor", Color(*self._theme.getColor("model_non_printing").getRgb())) self._non_printing_shader.setUniformValue("u_opacity", 0.6) if not self._support_mesh_shader: self._support_mesh_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "striped.shader")) self._support_mesh_shader.setUniformValue("u_vertical_stripes", True) self._support_mesh_shader.setUniformValue("u_width", 5.0) if not Application.getInstance().getPreferences().getValue(self._show_xray_warning_preference): self._xray_shader = None self._xray_composite_shader = None if self._composite_pass and 'xray' in self._composite_pass.getLayerBindings(): self._composite_pass.setLayerBindings(self._old_layer_bindings) self._composite_pass.setCompositeShader(self._old_composite_shader) self._old_layer_bindings = None self._old_composite_shader = None self._xray_warning_message.hide() else: if not self._xray_shader: self._xray_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "xray.shader")) if not self._xray_composite_shader: self._xray_composite_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "xray_composite.shader")) theme = Application.getInstance().getTheme() self._xray_composite_shader.setUniformValue("u_background_color", Color(*theme.getColor("viewport_background").getRgb())) self._xray_composite_shader.setUniformValue("u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb())) renderer = self.getRenderer() if not self._composite_pass or not 'xray' in self._composite_pass.getLayerBindings(): # Currently the RenderPass constructor requires a size > 0 # This should be fixed in RenderPass's constructor. self._xray_pass = XRayPass.XRayPass(1, 1) renderer.addRenderPass(self._xray_pass) if not self._composite_pass: self._composite_pass = self.getRenderer().getRenderPass("composite") self._old_layer_bindings = self._composite_pass.getLayerBindings() self._composite_pass.setLayerBindings(["default", "selection", "xray"]) self._old_composite_shader = self._composite_pass.getCompositeShader() self._composite_pass.setCompositeShader(self._xray_composite_shader)
class BuildVolume(SceneNode): VolumeOutlineColor = Color(12, 169, 227, 255) raftThicknessChanged = Signal() def __init__(self, parent=None): super().__init__(parent) self._width = 0 self._height = 0 self._depth = 0 self._shader = None self._grid_mesh = None self._grid_shader = None self._disallowed_areas = [] self._disallowed_area_mesh = None self.setCalculateBoundingBox(False) self._volume_aabb = None self._raft_thickness = 0.0 self._adhesion_type = None self._platform = Platform(self) self._global_container_stack = None Application.getInstance().globalContainerStackChanged.connect( self._onGlobalContainerStackChanged) self._onGlobalContainerStackChanged() self._active_extruder_stack = None ExtruderManager.getInstance().activeExtruderChanged.connect( self._onActiveExtruderStackChanged) self._onActiveExtruderStackChanged() def setWidth(self, width): if width: self._width = width def setHeight(self, height): if height: self._height = height def setDepth(self, depth): if depth: self._depth = depth def getDisallowedAreas(self): return self._disallowed_areas def setDisallowedAreas(self, areas): self._disallowed_areas = areas def render(self, renderer): if not self.getMeshData(): return True if not self._shader: self._shader = OpenGL.getInstance().createShaderProgram( Resources.getPath(Resources.Shaders, "default.shader")) self._grid_shader = OpenGL.getInstance().createShaderProgram( Resources.getPath(Resources.Shaders, "grid.shader")) renderer.queueNode(self, mode=RenderBatch.RenderMode.Lines) renderer.queueNode(self, mesh=self._grid_mesh, shader=self._grid_shader, backface_cull=True) if self._disallowed_area_mesh: renderer.queueNode(self, mesh=self._disallowed_area_mesh, shader=self._shader, transparent=True, backface_cull=True, sort=-9) return True ## Recalculates the build volume & disallowed areas. def rebuild(self): if not self._width or not self._height or not self._depth: return min_w = -self._width / 2 max_w = self._width / 2 min_h = 0.0 max_h = self._height min_d = -self._depth / 2 max_d = self._depth / 2 mb = MeshBuilder() # Outline 'cube' of the build volume mb.addLine(Vector(min_w, min_h, min_d), Vector(max_w, min_h, min_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, max_h, min_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, max_h, min_d), Vector(max_w, max_h, min_d), color=self.VolumeOutlineColor) mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, max_h, min_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, max_d), Vector(max_w, min_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, max_d), Vector(min_w, max_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, max_h, max_d), Vector(max_w, max_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(max_w, min_h, max_d), Vector(max_w, max_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, min_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, min_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color=self.VolumeOutlineColor) mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color=self.VolumeOutlineColor) self.setMeshData(mb.build()) mb = MeshBuilder() mb.addQuad(Vector(min_w, min_h - 0.2, min_d), Vector(max_w, min_h - 0.2, min_d), Vector(max_w, min_h - 0.2, max_d), Vector(min_w, min_h - 0.2, max_d)) for n in range(0, 6): v = mb.getVertex(n) mb.setVertexUVCoordinates(n, v[0], v[2]) self._grid_mesh = mb.build() disallowed_area_height = 0.1 disallowed_area_size = 0 if self._disallowed_areas: mb = MeshBuilder() color = Color(0.0, 0.0, 0.0, 0.15) for polygon in self._disallowed_areas: points = polygon.getPoints() first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) previous_point = Vector( self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) for point in points: new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height, self._clamp(point[1], min_d, max_d)) mb.addFace(first, previous_point, new_point, color=color) previous_point = new_point # Find the largest disallowed area to exclude it from the maximum scale bounds. # This is a very nasty hack. This pretty much only works for UM machines. # This disallowed area_size needs a -lot- of rework at some point in the future: TODO if numpy.min( points[:, 1] ) >= 0: # This filters out all areas that have points to the left of the centre. This is done to filter the skirt area. size = abs( numpy.max(points[:, 1]) - numpy.min(points[:, 1])) else: size = 0 disallowed_area_size = max(size, disallowed_area_size) self._disallowed_area_mesh = mb.build() else: self._disallowed_area_mesh = None self._volume_aabb = AxisAlignedBox( minimum=Vector(min_w, min_h - 1.0, min_d), maximum=Vector(max_w, max_h - self._raft_thickness, max_d)) bed_adhesion_size = 0.0 container_stack = Application.getInstance().getGlobalContainerStack() if container_stack: bed_adhesion_size = self._getBedAdhesionSize(container_stack) # As this works better for UM machines, we only add the disallowed_area_size for the z direction. # This is probably wrong in all other cases. TODO! # The +1 and -1 is added as there is always a bit of extra room required to work properly. scale_to_max_bounds = AxisAlignedBox( minimum=Vector( min_w + bed_adhesion_size + 1, min_h, min_d + disallowed_area_size - bed_adhesion_size + 1), maximum=Vector( max_w - bed_adhesion_size - 1, max_h - self._raft_thickness, max_d - disallowed_area_size + bed_adhesion_size - 1)) Application.getInstance().getController().getScene( )._maximum_bounds = scale_to_max_bounds def getBoundingBox(self): return self._volume_aabb def _buildVolumeMessage(self): Message( catalog.i18nc( "@info:status", "The build volume height has been reduced due to the value of the" " \"Print Sequence\" setting to prevent the gantry from colliding" " with printed models.")).show() def getRaftThickness(self): return self._raft_thickness def _updateRaftThickness(self): old_raft_thickness = self._raft_thickness self._adhesion_type = self._global_container_stack.getProperty( "adhesion_type", "value") self._raft_thickness = 0.0 if self._adhesion_type == "raft": self._raft_thickness = (self._global_container_stack.getProperty( "raft_base_thickness", "value") + self._global_container_stack.getProperty( "raft_interface_thickness", "value") + self._global_container_stack.getProperty( "raft_surface_layers", "value") * self._global_container_stack.getProperty( "raft_surface_thickness", "value") + self._global_container_stack.getProperty( "raft_airgap", "value")) # Rounding errors do not matter, we check if raft_thickness has changed at all if old_raft_thickness != self._raft_thickness: self.setPosition(Vector(0, -self._raft_thickness, 0), SceneNode.TransformSpace.World) self.raftThicknessChanged.emit() def _onGlobalContainerStackChanged(self): if self._global_container_stack: self._global_container_stack.propertyChanged.disconnect( self._onSettingPropertyChanged) self._global_container_stack = Application.getInstance( ).getGlobalContainerStack() if self._global_container_stack: self._global_container_stack.propertyChanged.connect( self._onSettingPropertyChanged) self._width = self._global_container_stack.getProperty( "machine_width", "value") machine_height = self._global_container_stack.getProperty( "machine_height", "value") if self._global_container_stack.getProperty( "print_sequence", "value") == "one_at_a_time": self._height = min( self._global_container_stack.getProperty( "gantry_height", "value"), machine_height) if self._height < machine_height: self._buildVolumeMessage() else: self._height = self._global_container_stack.getProperty( "machine_height", "value") self._depth = self._global_container_stack.getProperty( "machine_depth", "value") self._updateDisallowedAreas() self._updateRaftThickness() self.rebuild() def _onActiveExtruderStackChanged(self): if self._active_extruder_stack: self._active_extruder_stack.propertyChanged.disconnect( self._onSettingPropertyChanged) self._active_extruder_stack = ExtruderManager.getInstance( ).getActiveExtruderStack() if self._active_extruder_stack: self._active_extruder_stack.propertyChanged.connect( self._onSettingPropertyChanged) def _onSettingPropertyChanged(self, setting_key, property_name): if property_name != "value": return rebuild_me = False if setting_key == "print_sequence": machine_height = self._global_container_stack.getProperty( "machine_height", "value") if Application.getInstance().getGlobalContainerStack().getProperty( "print_sequence", "value") == "one_at_a_time": self._height = min( self._global_container_stack.getProperty( "gantry_height", "value"), machine_height) if self._height < machine_height: self._buildVolumeMessage() else: self._height = self._global_container_stack.getProperty( "machine_height", "value") rebuild_me = True if setting_key in self._skirt_settings or setting_key in self._prime_settings or setting_key in self._tower_settings: self._updateDisallowedAreas() rebuild_me = True if setting_key in self._raft_settings: self._updateRaftThickness() rebuild_me = True if rebuild_me: self.rebuild() def _updateDisallowedAreas(self): if not self._global_container_stack: return disallowed_areas = copy.deepcopy( self._global_container_stack.getProperty( "machine_disallowed_areas", "value")) areas = [] machine_width = self._global_container_stack.getProperty( "machine_width", "value") machine_depth = self._global_container_stack.getProperty( "machine_depth", "value") # Add prime tower location as disallowed area. if self._global_container_stack.getProperty("prime_tower_enable", "value") == True: prime_tower_size = self._global_container_stack.getProperty( "prime_tower_size", "value") prime_tower_x = self._global_container_stack.getProperty( "prime_tower_position_x", "value") - machine_width / 2 prime_tower_y = -self._global_container_stack.getProperty( "prime_tower_position_y", "value") + machine_depth / 2 disallowed_areas.append([ [ prime_tower_x - prime_tower_size, prime_tower_y - prime_tower_size ], [prime_tower_x, prime_tower_y - prime_tower_size], [prime_tower_x, prime_tower_y], [prime_tower_x - prime_tower_size, prime_tower_y], ]) # Add extruder prime locations as disallowed areas. # Probably needs some rework after coordinate system change. extruder_manager = ExtruderManager.getInstance() extruders = extruder_manager.getMachineExtruders( self._global_container_stack.getId()) for single_extruder in extruders: extruder_prime_pos_x = single_extruder.getProperty( "extruder_prime_pos_x", "value") extruder_prime_pos_y = single_extruder.getProperty( "extruder_prime_pos_y", "value") # TODO: calculate everything in CuraEngine/Firmware/lower left as origin coordinates. # Here we transform the extruder prime pos (lower left as origin) to Cura coordinates # (center as origin, y from back to front) prime_x = extruder_prime_pos_x - machine_width / 2 prime_y = machine_depth / 2 - extruder_prime_pos_y disallowed_areas.append([ [prime_x - PRIME_CLEARANCE, prime_y - PRIME_CLEARANCE], [prime_x + PRIME_CLEARANCE, prime_y - PRIME_CLEARANCE], [prime_x + PRIME_CLEARANCE, prime_y + PRIME_CLEARANCE], [prime_x - PRIME_CLEARANCE, prime_y + PRIME_CLEARANCE], ]) bed_adhesion_size = self._getBedAdhesionSize( self._global_container_stack) if disallowed_areas: # Extend every area already in the disallowed_areas with the skirt size. for area in disallowed_areas: poly = Polygon(numpy.array(area, numpy.float32)) poly = poly.getMinkowskiHull( Polygon(approximatedCircleVertices(bed_adhesion_size))) areas.append(poly) # Add the skirt areas around the borders of the build plate. if bed_adhesion_size > 0: half_machine_width = self._global_container_stack.getProperty( "machine_width", "value") / 2 half_machine_depth = self._global_container_stack.getProperty( "machine_depth", "value") / 2 areas.append( Polygon( numpy.array([[-half_machine_width, -half_machine_depth], [-half_machine_width, half_machine_depth], [ -half_machine_width + bed_adhesion_size, half_machine_depth - bed_adhesion_size ], [ -half_machine_width + bed_adhesion_size, -half_machine_depth + bed_adhesion_size ]], numpy.float32))) areas.append( Polygon( numpy.array([[half_machine_width, half_machine_depth], [half_machine_width, -half_machine_depth], [ half_machine_width - bed_adhesion_size, -half_machine_depth + bed_adhesion_size ], [ half_machine_width - bed_adhesion_size, half_machine_depth - bed_adhesion_size ]], numpy.float32))) areas.append( Polygon( numpy.array([[-half_machine_width, half_machine_depth], [half_machine_width, half_machine_depth], [ half_machine_width - bed_adhesion_size, half_machine_depth - bed_adhesion_size ], [ -half_machine_width + bed_adhesion_size, half_machine_depth - bed_adhesion_size ]], numpy.float32))) areas.append( Polygon( numpy.array([[half_machine_width, -half_machine_depth], [-half_machine_width, -half_machine_depth], [ -half_machine_width + bed_adhesion_size, -half_machine_depth + bed_adhesion_size ], [ half_machine_width - bed_adhesion_size, -half_machine_depth + bed_adhesion_size ]], numpy.float32))) self._disallowed_areas = areas ## Convenience function to calculate the size of the bed adhesion in directions x, y. def _getBedAdhesionSize(self, container_stack): skirt_size = 0.0 adhesion_type = container_stack.getProperty("adhesion_type", "value") if adhesion_type == "skirt": skirt_distance = container_stack.getProperty("skirt_gap", "value") skirt_line_count = container_stack.getProperty( "skirt_line_count", "value") skirt_size = skirt_distance + ( skirt_line_count * container_stack.getProperty("skirt_brim_line_width", "value")) elif adhesion_type == "brim": skirt_size = container_stack.getProperty( "brim_line_count", "value") * container_stack.getProperty( "skirt_brim_line_width", "value") elif adhesion_type == "raft": skirt_size = container_stack.getProperty("raft_margin", "value") if container_stack.getProperty("draft_shield_enabled", "value"): skirt_size += container_stack.getProperty("draft_shield_dist", "value") if container_stack.getProperty("xy_offset", "value"): skirt_size += container_stack.getProperty("xy_offset", "value") return skirt_size def _clamp(self, value, min_value, max_value): return max(min(value, max_value), min_value) _skirt_settings = [ "adhesion_type", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist", "xy_offset" ] _raft_settings = [ "adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap" ] _prime_settings = [ "extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z" ] _tower_settings = [ "prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y" ]
class ToolHandle(SceneNode.SceneNode): """A tool handle is a object in the scene that gives queues for what the tool it is 'paired' with can do. ToolHandles are, for example, used for translation, rotation & scale handles. They can also be used as actual objects to interact with (in the case of translation, pressing one arrow of the toolhandle locks the translation in that direction) """ NoAxis = 1 XAxis = 2 YAxis = 3 ZAxis = 4 AllAxis = 5 # These colors are used to draw the selection pass only. They must be unique, which is # why we cannot rely on themed colors DisabledSelectionColor = Color(0.5, 0.5, 0.5, 1.0) XAxisSelectionColor = Color(1.0, 0.0, 0.0, 1.0) YAxisSelectionColor = Color(0.0, 0.0, 1.0, 1.0) ZAxisSelectionColor = Color(0.0, 1.0, 0.0, 1.0) AllAxisSelectionColor = Color(1.0, 1.0, 1.0, 1.0) def __init__(self, parent=None): super().__init__(parent) self._disabled_axis_color = None self._x_axis_color = None self._y_axis_color = None self._z_axis_color = None self._all_axis_color = None self._axis_color_map = {} self._scene = Application.getInstance().getController().getScene() self._solid_mesh = None # type: Optional[MeshData] self._line_mesh = None # type: Optional[MeshData] self._selection_mesh = None # type: Optional[MeshData] self._shader = None self._active_axis = None # type: Optional[int] # Auto scale is used to ensure that the tool handle will end up the same size on the camera no matter the zoom # This should be used to ensure that the tool handles are still usable even if the camera is zoomed in all the way. self._auto_scale = True self._enabled = False self.setCalculateBoundingBox(False) Selection.selectionCenterChanged.connect( self._onSelectionCenterChanged) Application.getInstance().engineCreatedSignal.connect( self._onEngineCreated) def getLineMesh(self) -> Optional[MeshData]: return self._line_mesh def setLineMesh(self, mesh: MeshData) -> None: self._line_mesh = mesh self.meshDataChanged.emit(self) def getSolidMesh(self) -> Optional[MeshData]: return self._solid_mesh def setSolidMesh(self, mesh: MeshData) -> None: self._solid_mesh = mesh self.meshDataChanged.emit(self) def getSelectionMesh(self) -> Optional[MeshData]: return self._selection_mesh def setSelectionMesh(self, mesh: MeshData) -> None: self._selection_mesh = mesh self.meshDataChanged.emit(self) def render(self, renderer) -> bool: if not self._enabled: return True if not self._shader: self._shader = OpenGL.getInstance().createShaderProgram( Resources.getPath(Resources.Shaders, "toolhandle.shader")) if self._auto_scale: active_camera = self._scene.getActiveCamera() if active_camera.isPerspective(): camera_position = active_camera.getWorldPosition() dist = (camera_position - self.getWorldPosition()).length() scale = dist / 400 else: view_width = active_camera.getViewportWidth() current_size = view_width + ( 2 * active_camera.getZoomFactor() * view_width) scale = current_size / view_width * 5 self.setScale(Vector(scale, scale, scale)) if self._line_mesh: renderer.queueNode(self, mesh=self._line_mesh, mode=RenderBatch.RenderMode.Lines, overlay=True, shader=self._shader) if self._solid_mesh: renderer.queueNode(self, mesh=self._solid_mesh, overlay=True, shader=self._shader) return True def setActiveAxis(self, axis: Optional[int]) -> None: if axis == self._active_axis or not self._shader: return if axis: self._shader.setUniformValue( "u_activeColor", self._axis_color_map.get(axis, Color())) else: self._shader.setUniformValue("u_activeColor", self._disabled_axis_color) self._active_axis = axis self._scene.sceneChanged.emit(self) def getActiveAxis(self) -> Optional[int]: return self._active_axis def isAxis(self, value): return value in self._axis_color_map def buildMesh(self) -> None: # This method should be overridden by toolhandle implementations pass def _onSelectionCenterChanged(self) -> None: if self._enabled: self.setPosition(Selection.getSelectionCenter()) def setEnabled(self, enable: bool): super().setEnabled(enable) # Force an update self._onSelectionCenterChanged() def _onEngineCreated(self) -> None: from UM.Qt.QtApplication import QtApplication theme = QtApplication.getInstance().getTheme() if theme is None: Logger.log( "w", "Could not get theme, so unable to create tool handle meshes.") return self._disabled_axis_color = Color( *theme.getColor("disabled_axis").getRgb()) self._x_axis_color = Color(*theme.getColor("x_axis").getRgb()) self._y_axis_color = Color(*theme.getColor("y_axis").getRgb()) self._z_axis_color = Color(*theme.getColor("z_axis").getRgb()) self._all_axis_color = Color(*theme.getColor("all_axis").getRgb()) self._axis_color_map = { self.NoAxis: self._disabled_axis_color, self.XAxis: self._x_axis_color, self.YAxis: self._y_axis_color, self.ZAxis: self._z_axis_color, self.AllAxis: self._all_axis_color } self.buildMesh()
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(): 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 test_fromHexString(data): color = Color.fromHexString(data["data_to_set"]) expected_color = Color(*data["expected"]) assert color == expected_color