mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-06-26 09:25:24 -06:00
Adding the DryWise postprocessing script to the Cura repo.
PP-649
This commit is contained in:
parent
e627d09b1d
commit
e7472a83e4
5 changed files with 2381 additions and 0 deletions
344
plugins/PostProcessingPlugin/scripts/HackedStartSliceJob.py
Normal file
344
plugins/PostProcessingPlugin/scripts/HackedStartSliceJob.py
Normal file
|
@ -0,0 +1,344 @@
|
|||
try:
|
||||
from plugins.CuraEngineBackend.StartSliceJob import *
|
||||
from plugins.CuraEngineBackend.CuraEngineBackend import *
|
||||
except:
|
||||
from share.cura.plugins.CuraEngineBackend.StartSliceJob import *
|
||||
from share.cura.plugins.CuraEngineBackend.CuraEngineBackend import *
|
||||
|
||||
|
||||
class HackedStartSliceJob(StartSliceJob):
|
||||
def run(self) -> None:
|
||||
"""Runs the job that initiates the slicing."""
|
||||
|
||||
if self._build_plate_number is None:
|
||||
self.setResult(StartJobResult.Error)
|
||||
return
|
||||
|
||||
stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if not stack:
|
||||
self.setResult(StartJobResult.Error)
|
||||
return
|
||||
|
||||
# Don't slice if there is a setting with an error value.
|
||||
if CuraApplication.getInstance().getMachineManager().stacksHaveErrors:
|
||||
self.setResult(StartJobResult.SettingError)
|
||||
return
|
||||
|
||||
if CuraApplication.getInstance().getBuildVolume().hasErrors():
|
||||
self.setResult(StartJobResult.BuildPlateError)
|
||||
return
|
||||
|
||||
# Wait for error checker to be done.
|
||||
while CuraApplication.getInstance().getMachineErrorChecker().needToWaitForResult:
|
||||
time.sleep(0.1)
|
||||
|
||||
if CuraApplication.getInstance().getMachineErrorChecker().hasError:
|
||||
self.setResult(StartJobResult.SettingError)
|
||||
return
|
||||
|
||||
# Don't slice if the buildplate or the nozzle type is incompatible with the materials
|
||||
if not CuraApplication.getInstance().getMachineManager().variantBuildplateCompatible and \
|
||||
not CuraApplication.getInstance().getMachineManager().variantBuildplateUsable:
|
||||
self.setResult(StartJobResult.MaterialIncompatible)
|
||||
return
|
||||
|
||||
for extruder_stack in stack.extruderList:
|
||||
material = extruder_stack.findContainer({"type": "material"})
|
||||
if not extruder_stack.isEnabled:
|
||||
continue
|
||||
if material:
|
||||
if material.getMetaDataEntry("compatible") == False:
|
||||
self.setResult(StartJobResult.MaterialIncompatible)
|
||||
return
|
||||
|
||||
# Don't slice if there is a per object setting with an error value.
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if not isinstance(node, CuraSceneNode) or not node.isSelectable():
|
||||
continue
|
||||
|
||||
if self._checkStackForErrors(node.callDecoration("getStack")):
|
||||
self.setResult(StartJobResult.ObjectSettingError)
|
||||
return
|
||||
|
||||
# Remove old layer data.
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if node.callDecoration("getLayerData") and node.callDecoration(
|
||||
"getBuildPlateNumber") == self._build_plate_number:
|
||||
# Since we walk through all nodes in the scene, they always have a parent.
|
||||
cast(SceneNode, node.getParent()).removeChild(node)
|
||||
break
|
||||
|
||||
# Get the objects in their groups to print.
|
||||
object_groups = []
|
||||
if stack.getProperty("print_sequence", "value") == "one_at_a_time":
|
||||
modifier_mesh_nodes = []
|
||||
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
build_plate_number = node.callDecoration("getBuildPlateNumber")
|
||||
if node.callDecoration("isNonPrintingMesh") and build_plate_number == self._build_plate_number:
|
||||
modifier_mesh_nodes.append(node)
|
||||
|
||||
for node in OneAtATimeIterator(self._scene.getRoot()):
|
||||
temp_list = []
|
||||
|
||||
# Filter on current build plate
|
||||
build_plate_number = node.callDecoration("getBuildPlateNumber")
|
||||
if build_plate_number is not None and build_plate_number != self._build_plate_number:
|
||||
continue
|
||||
|
||||
children = node.getAllChildren()
|
||||
children.append(node)
|
||||
for child_node in children:
|
||||
mesh_data = child_node.getMeshData()
|
||||
if mesh_data and mesh_data.getVertices() is not None:
|
||||
temp_list.append(child_node)
|
||||
|
||||
if temp_list:
|
||||
object_groups.append(temp_list + modifier_mesh_nodes)
|
||||
Job.yieldThread()
|
||||
if len(object_groups) == 0:
|
||||
Logger.log("w", "No objects suitable for one at a time found, or no correct order found")
|
||||
else:
|
||||
temp_list = []
|
||||
has_printing_mesh = False
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
mesh_data = node.getMeshData()
|
||||
if node.callDecoration("isSliceable") and mesh_data and mesh_data.getVertices() is not None:
|
||||
is_non_printing_mesh = bool(node.callDecoration("isNonPrintingMesh"))
|
||||
|
||||
# Find a reason not to add the node
|
||||
if node.callDecoration("getBuildPlateNumber") != self._build_plate_number:
|
||||
continue
|
||||
if getattr(node, "_outside_buildarea", False) and not is_non_printing_mesh:
|
||||
continue
|
||||
|
||||
temp_list.append(node)
|
||||
if not is_non_printing_mesh:
|
||||
has_printing_mesh = True
|
||||
|
||||
Job.yieldThread()
|
||||
|
||||
# If the list doesn't have any model with suitable settings then clean the list
|
||||
# otherwise CuraEngine will crash
|
||||
if not has_printing_mesh:
|
||||
temp_list.clear()
|
||||
|
||||
if temp_list:
|
||||
object_groups.append(temp_list)
|
||||
|
||||
if stack.getProperty("print_sequence", "value") == "one_at_a_time":
|
||||
idx = 0
|
||||
while 'drywise_purge_box' not in object_groups[idx][0].getName():
|
||||
idx += 1
|
||||
if len(object_groups) <= idx:
|
||||
break
|
||||
else:
|
||||
object_groups[0], object_groups[idx] = object_groups[idx], object_groups[0]
|
||||
else:
|
||||
group = object_groups[0]
|
||||
idx = 0
|
||||
while 'drywise_purge_box' not in group[idx].getName():
|
||||
idx += 1
|
||||
if len(group) <= idx:
|
||||
break
|
||||
else:
|
||||
object_groups = [[group.pop(idx)], group]
|
||||
|
||||
global_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return
|
||||
extruders_enabled = [stack.isEnabled for stack in global_stack.extruderList]
|
||||
filtered_object_groups = []
|
||||
has_model_with_disabled_extruders = False
|
||||
associated_disabled_extruders = set()
|
||||
for group in object_groups:
|
||||
stack = global_stack
|
||||
skip_group = False
|
||||
for node in group:
|
||||
# Only check if the printing extruder is enabled for printing meshes
|
||||
is_non_printing_mesh = node.callDecoration("evaluateIsNonPrintingMesh")
|
||||
extruder_position = int(node.callDecoration("getActiveExtruderPosition"))
|
||||
if not is_non_printing_mesh and not extruders_enabled[extruder_position]:
|
||||
skip_group = True
|
||||
has_model_with_disabled_extruders = True
|
||||
associated_disabled_extruders.add(extruder_position)
|
||||
if not skip_group:
|
||||
filtered_object_groups.append(group)
|
||||
|
||||
if has_model_with_disabled_extruders:
|
||||
self.setResult(StartJobResult.ObjectsWithDisabledExtruder)
|
||||
associated_disabled_extruders = {p + 1 for p in associated_disabled_extruders}
|
||||
self._associated_disabled_extruders = ", ".join(map(str, sorted(associated_disabled_extruders)))
|
||||
return
|
||||
|
||||
# There are cases when there is nothing to slice. This can happen due to one at a time slicing not being
|
||||
# able to find a possible sequence or because there are no objects on the build plate (or they are outside
|
||||
# the build volume)
|
||||
if not filtered_object_groups:
|
||||
self.setResult(StartJobResult.NothingToSlice)
|
||||
return
|
||||
|
||||
self._buildGlobalSettingsMessage(stack)
|
||||
self._buildGlobalInheritsStackMessage(stack)
|
||||
|
||||
user_id = uuid.getnode() # On all of Cura's supported platforms, this returns the MAC address which is pseudonymical information (!= anonymous).
|
||||
user_id %= 2 ** 16 # So to make it anonymous, apply a bitmask selecting only the last 16 bits. This prevents it from being traceable to a specific user but still gives somewhat of an idea of whether it's just the same user hitting the same crash over and over again, or if it's widespread.
|
||||
self._slice_message.sentry_id = f"{user_id}"
|
||||
self._slice_message.cura_version = CuraVersion
|
||||
|
||||
# Add the project name to the message if the user allows for non-anonymous crash data collection.
|
||||
account = CuraApplication.getInstance().getCuraAPI().account
|
||||
if account and account.isLoggedIn and not CuraApplication.getInstance().getPreferences().getValue(
|
||||
"info/anonymous_engine_crash_report"):
|
||||
self._slice_message.project_name = CuraApplication.getInstance().getPrintInformation().baseName
|
||||
self._slice_message.user_name = account.userName
|
||||
|
||||
# Build messages for extruder stacks
|
||||
for extruder_stack in global_stack.extruderList:
|
||||
self._buildExtruderMessage(extruder_stack)
|
||||
|
||||
backend_plugins = CuraApplication.getInstance().getBackendPlugins()
|
||||
|
||||
# Sort backend plugins by name. Not a very good strategy, but at least it is repeatable. This will be improved later.
|
||||
backend_plugins = sorted(backend_plugins, key=lambda backend_plugin: backend_plugin.getId())
|
||||
|
||||
for plugin in backend_plugins:
|
||||
if not plugin.usePlugin():
|
||||
continue
|
||||
for slot in plugin.getSupportedSlots():
|
||||
# Right now we just send the message for every slot that we support. A single plugin can support
|
||||
# multiple slots
|
||||
# In the future the frontend will need to decide what slots that a plugin actually supports should
|
||||
# also be used. For instance, if you have two plugins and each of them support a_generate and b_generate
|
||||
# only one of each can actually be used (eg; plugin 1 does both, plugin 1 does a_generate and 2 does
|
||||
# b_generate, etc).
|
||||
plugin_message = self._slice_message.addRepeatedMessage("engine_plugins")
|
||||
plugin_message.id = slot
|
||||
plugin_message.address = plugin.getAddress()
|
||||
plugin_message.port = plugin.getPort()
|
||||
plugin_message.plugin_name = plugin.getPluginId()
|
||||
plugin_message.plugin_version = plugin.getVersion()
|
||||
|
||||
for group in filtered_object_groups:
|
||||
group_message = self._slice_message.addRepeatedMessage("object_lists")
|
||||
parent = group[0].getParent()
|
||||
if parent is not None and parent.callDecoration("isGroup"):
|
||||
self._handlePerObjectSettings(cast(CuraSceneNode, parent), group_message)
|
||||
|
||||
for object in group:
|
||||
mesh_data = object.getMeshData()
|
||||
if mesh_data is None:
|
||||
continue
|
||||
rot_scale = object.getWorldTransformation().getTransposed().getData()[0:3, 0:3]
|
||||
translate = object.getWorldTransformation().getData()[:3, 3]
|
||||
|
||||
# This effectively performs a limited form of MeshData.getTransformed that ignores normals.
|
||||
verts = mesh_data.getVertices()
|
||||
verts = verts.dot(rot_scale)
|
||||
verts += translate
|
||||
|
||||
# Convert from Y up axes to Z up axes. Equals a 90 degree rotation.
|
||||
verts[:, [1, 2]] = verts[:, [2, 1]]
|
||||
verts[:, 1] *= -1
|
||||
|
||||
obj = group_message.addRepeatedMessage("objects")
|
||||
obj.id = id(object)
|
||||
obj.name = object.getName()
|
||||
indices = mesh_data.getIndices()
|
||||
if indices is not None:
|
||||
flat_verts = numpy.take(verts, indices.flatten(), axis=0)
|
||||
else:
|
||||
flat_verts = numpy.array(verts)
|
||||
|
||||
obj.vertices = flat_verts
|
||||
|
||||
self._handlePerObjectSettings(cast(CuraSceneNode, object), obj)
|
||||
|
||||
Job.yieldThread()
|
||||
|
||||
self.setResult(StartJobResult.Finished)
|
||||
|
||||
|
||||
def hacked_slice(self: CuraEngineBackend) -> None:
|
||||
"""Perform a slice of the scene."""
|
||||
|
||||
self._createSnapshot()
|
||||
|
||||
self.startPlugins()
|
||||
|
||||
Logger.log("i", "Starting to slice...")
|
||||
self._time_start_process = time()
|
||||
if not self._build_plates_to_be_sliced:
|
||||
self.processingProgress.emit(1.0)
|
||||
Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.")
|
||||
self.setState(BackendState.Done)
|
||||
return
|
||||
|
||||
if self._process_layers_job:
|
||||
Logger.log("d", "Process layers job still busy, trying later.")
|
||||
return
|
||||
|
||||
if not hasattr(self._scene, "gcode_dict"):
|
||||
self._scene.gcode_dict = {} # type: ignore
|
||||
# We need to ignore type because we are creating the missing attribute here.
|
||||
|
||||
# see if we really have to slice
|
||||
application = CuraApplication.getInstance()
|
||||
active_build_plate = application.getMultiBuildPlateModel().activeBuildPlate
|
||||
build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0)
|
||||
Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced)
|
||||
num_objects = self._numObjectsPerBuildPlate()
|
||||
|
||||
self._stored_layer_data = []
|
||||
|
||||
if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0:
|
||||
self._scene.gcode_dict[build_plate_to_be_sliced] = [] # type: ignore
|
||||
# We need to ignore the type because we created this attribute above.
|
||||
Logger.log("d", "Build plate %s has no objects to be sliced, skipping", build_plate_to_be_sliced)
|
||||
if self._build_plates_to_be_sliced:
|
||||
self.slice()
|
||||
return
|
||||
self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
|
||||
if application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
|
||||
application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
|
||||
|
||||
if self._process is None: # type: ignore
|
||||
self._createSocket()
|
||||
self.stopSlicing()
|
||||
self._engine_is_fresh = False # Yes we're going to use the engine
|
||||
|
||||
self.processingProgress.emit(0.0)
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
|
||||
self._scene.gcode_dict[build_plate_to_be_sliced] = [] # type: ignore #[] indexed by build plate number
|
||||
self._slicing = True
|
||||
self.slicingStarted.emit()
|
||||
|
||||
self.determineAutoSlicing() # Switch timer on or off if appropriate
|
||||
|
||||
slice_message = self._socket.createMessage("cura.proto.Slice")
|
||||
self._start_slice_job = HackedStartSliceJob(slice_message)
|
||||
self._start_slice_job_build_plate = build_plate_to_be_sliced
|
||||
self._start_slice_job.setBuildPlate(self._start_slice_job_build_plate)
|
||||
self._start_slice_job.start()
|
||||
self._start_slice_job.finished.connect(self._onStartSliceCompleted)
|
||||
|
||||
|
||||
def apply_hack():
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
|
||||
registry = PluginRegistry.getInstance()
|
||||
backend: CuraEngineBackend = registry.getPluginObject('CuraEngineBackend')
|
||||
backend.slice = lambda b=backend: hacked_slice(backend)
|
||||
|
||||
# def apply_hack():
|
||||
# def decor(func):
|
||||
# def wrapper(*args, **kwargs):
|
||||
# print('wrapped')
|
||||
# func(*args, **kwargs)
|
||||
# print('wrapped')
|
||||
# return wrapper
|
||||
#
|
||||
# StartSliceJob.run = decor(hacked_run)
|
||||
# print('hacked')
|
||||
|
512
plugins/PostProcessingPlugin/scripts/PurgeCubeNode.py
Normal file
512
plugins/PostProcessingPlugin/scripts/PurgeCubeNode.py
Normal file
|
@ -0,0 +1,512 @@
|
|||
import math
|
||||
import time
|
||||
from pathlib import Path
|
||||
import typing
|
||||
import numpy as np
|
||||
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Math.Quaternion import Quaternion
|
||||
from UM.Math.Polygon import Polygon
|
||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
from UM.FileHandler.ReadFileJob import ReadFileJob
|
||||
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
|
||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||
from cura.Settings.PerObjectContainerStack import PerObjectContainerStack
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Settings.SettingInstance import SettingInstance
|
||||
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||
|
||||
|
||||
from PyQt6.QtCore import QUrl
|
||||
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
from UM.Mesh.ReadMeshJob import ReadMeshJob
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from UseDrywiseDryer import UseDrywiseDryer
|
||||
|
||||
|
||||
|
||||
class PurgeCubeSceneNode(CuraSceneNode):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.drywise_script: UseDrywiseDryer | None = None
|
||||
|
||||
self.prev_size: np.ndarray | None = None
|
||||
self.prev_orient: Quaternion | None = None
|
||||
self._current_volume: float = 1.0
|
||||
self._initial_size: np.ndarray | None = None
|
||||
|
||||
self._size_bounds: np.ndarray | None = None
|
||||
self.transformationChanged.connect(self._on_transform)
|
||||
|
||||
# Signals on adding an object
|
||||
# Signals when selecting an object
|
||||
# Signals when deselecting an object
|
||||
# Signals when selecting which extruder should print the object
|
||||
# extruder_manager.selectedObjectExtrudersChanged.connect(self.on_extruder_change)
|
||||
|
||||
if (app := CuraApplication.getInstance()) is None:
|
||||
return
|
||||
root_node = app.getController().getScene().getRoot()
|
||||
root_node.childrenChanged.connect(self.verify_if_still_exists)
|
||||
|
||||
def set_setting(self, key: str, value: typing.Any, property_name: str = 'value'):
|
||||
""" Sets per object slicing setting (or adds it if it is not there) """
|
||||
|
||||
if not self.hasDecoration('getStack'):
|
||||
raise RuntimeError(f'{self} has no decoration `getStack`!')
|
||||
# A container stack that just contains `userChanges` which is a single InstanceContainer
|
||||
node_stack = typing.cast(PerObjectContainerStack, self.callDecoration('getStack'))
|
||||
user_changes = typing.cast(InstanceContainer, node_stack.userChanges)
|
||||
if not user_changes.hasProperty(key, property_name):
|
||||
app = CuraApplication.getInstance()
|
||||
stack = app.getGlobalContainerStack()
|
||||
if (definition := stack.getSettingDefinition(key)) is None:
|
||||
Logger.warning(f'No definition for the {key=}.')
|
||||
return
|
||||
# Adding a setting like this will make it visible to the user in the per-object settings.
|
||||
user_changes.addInstance(SettingInstance(definition, container=user_changes))
|
||||
user_changes.setProperty(key, property_name, value)
|
||||
|
||||
def verify_if_still_exists(self, *args):
|
||||
""" Check if the user deleted this node. If so, then signal to the script to delete itself and this node """
|
||||
if (app := CuraApplication.getInstance()) is None:
|
||||
return
|
||||
root_node = app.getController().getScene().getRoot()
|
||||
|
||||
for child in root_node.getChildren():
|
||||
if child is self:
|
||||
return
|
||||
root_node.childrenChanged.disconnect(self.verify_if_still_exists)
|
||||
self.drywise_script.unload()
|
||||
|
||||
@property
|
||||
def size_bounds(self) -> np.ndarray:
|
||||
return self._size_bounds
|
||||
|
||||
@size_bounds.setter
|
||||
def size_bounds(self, value: np.ndarray):
|
||||
if value.shape != (2, 3):
|
||||
raise ValueError('Incorrect shape of the size bounds of the purge cube')
|
||||
if self._size_bounds is not None and np.allclose(self._size_bounds, value):
|
||||
return
|
||||
self._size_bounds = value
|
||||
self._on_transform()
|
||||
|
||||
@property
|
||||
def volume(self) -> float:
|
||||
""" Required volume of the purge model
|
||||
|
||||
Can change when the user changes some script parameters, such as distance from drywise output to extruder.
|
||||
As a result, the script should be able to update the scale of the model.
|
||||
|
||||
See the script paramters for more info on which parameters affect the final volume of the purge model.
|
||||
"""
|
||||
return self._current_volume
|
||||
|
||||
@volume.setter
|
||||
def volume(self, value: float):
|
||||
if np.isclose(value, self.volume):
|
||||
return
|
||||
self._current_volume = value
|
||||
xy_size_change = math.sqrt(value / self.volume)
|
||||
self.scale(Vector(xy_size_change, 1.0, xy_size_change), transform_space=SceneNode.TransformSpace.Local)
|
||||
|
||||
@property
|
||||
def size(self) -> np.ndarray:
|
||||
if self._initial_size is None:
|
||||
bbox = self.getBoundingBox()
|
||||
self._initial_size = np.array([bbox.width, bbox.height, bbox.depth])
|
||||
return self._initial_size * self.getScale().getData()
|
||||
|
||||
@size.setter
|
||||
def size(self, new_size: np.ndarray):
|
||||
if np.allclose(new_size, (current_size := self.size)):
|
||||
return
|
||||
# Scaling is done with respect to local coordinates and not the world coords, unlike with position and
|
||||
# orientation. The reason for this is that when scaling is
|
||||
self.scale(Vector(data=new_size/current_size), transform_space=SceneNode.TransformSpace.Local)
|
||||
|
||||
@property
|
||||
def z_height(self) -> float:
|
||||
return self.getBoundingBox().height
|
||||
|
||||
@z_height.setter
|
||||
def z_height(self, value: float):
|
||||
if np.isclose(value, self.z_height):
|
||||
return
|
||||
self.scale(Vector(1.0, value/self.z_height, 1.0), transform_space=SceneNode.TransformSpace.Local)
|
||||
|
||||
def place_and_resize_at(self,
|
||||
pos: typing.Literal['left', 'right', 'top', 'bottom'], bed_offset: float = 5.0, min_width: float = 10.0
|
||||
):
|
||||
global_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
build_volume = (
|
||||
float(global_stack.getProperty('machine_width', 'value')) - 2*bed_offset,
|
||||
float(global_stack.getProperty('machine_depth', 'value')) - 2*bed_offset,
|
||||
)
|
||||
|
||||
vol = self.volume
|
||||
# The height of the cube does not change
|
||||
height = self.z_height
|
||||
|
||||
# As a rule, if the position is on left/right, then size of the cube extends along the Y axis as much as
|
||||
# possible before extending along the X axis
|
||||
is_along_Y = pos in ['left', 'right']
|
||||
area = vol / height
|
||||
length = min(area / min_width, build_volume[int(is_along_Y)])
|
||||
width = max(min_width, area / length)
|
||||
|
||||
self.size = np.array([length, height, width])
|
||||
if is_along_Y:
|
||||
# Rotate 90 degrees cw if right and acw if left
|
||||
q = Quaternion()
|
||||
q.setByAngleAxis(angle=math.radians(90), axis=Vector(0, 1, 0))
|
||||
self.rotate(q, transform_space=SceneNode.TransformSpace.World)
|
||||
# By default, the positioning of the cube is in the center
|
||||
self.setPosition(Vector(
|
||||
x=0.0 if not is_along_Y else (build_volume[0]/2 - width) * (-1 if pos == 'left' else 1),
|
||||
z=0.0,
|
||||
y=0.0 if is_along_Y else (build_volume[0]/2 - width) * (-1 if pos == 'bottom' else 1),
|
||||
), transform_space=SceneNode.TransformSpace.World)
|
||||
|
||||
def _on_transform(self, *args):
|
||||
# Disconnect to prevent repeated firing if this method modifies transformations
|
||||
self.transformationChanged.disconnect(self._on_transform)
|
||||
|
||||
self.prev_size = self.prev_size if self.prev_size is not None else self.size
|
||||
self.prev_orient = self.prev_orient if self.prev_orient is not None else self.getWorldOrientation()
|
||||
|
||||
self._on_transform_forbid_xz_rotations()
|
||||
self._on_transform_maintain_size_bounds()
|
||||
self._on_transform_maintain_volume()
|
||||
|
||||
self.prev_size = self.size
|
||||
self.prev_orient = self.getWorldOrientation()
|
||||
|
||||
# Reconnect after corrections are applied
|
||||
self.transformationChanged.connect(self._on_transform)
|
||||
|
||||
def _on_transform_maintain_volume(self, *args):
|
||||
""" Maintains required volume for the purge cube """
|
||||
|
||||
size = self.size
|
||||
is_transformed = ~np.isclose(self.prev_size, size)
|
||||
if not np.any(is_transformed):
|
||||
return
|
||||
|
||||
volume_change = np.prod(size) / self.volume
|
||||
if is_transformed[1] or (is_transformed[0] and is_transformed[2]):
|
||||
scale_change = np.sqrt(np.array([volume_change, 1.0, volume_change]))
|
||||
elif is_transformed[0]:
|
||||
scale_change = np.array([1.0, 1.0, volume_change])
|
||||
else:
|
||||
scale_change = np.array([volume_change, 1.0, 1.0])
|
||||
self.size = size / scale_change
|
||||
|
||||
def _on_transform_maintain_size_bounds(self, *args):
|
||||
"""
|
||||
Ensure that the object does not extend outside of the buildplate
|
||||
|
||||
Notice that this does not protect against extending the object s.t. the adhesion area are either outside
|
||||
of the buildplate or placed on the non-printable area.
|
||||
"""
|
||||
if self.size_bounds is None:
|
||||
return
|
||||
|
||||
# `self.size` returns the size of the object in the local coordinate system, which is not
|
||||
# suitable for us, since the model can be rotated. As a result, it is best to compute the bbox
|
||||
# of the object on the buildplate and work with that.
|
||||
bbox = self.getBoundingBox()
|
||||
global_size = [bbox.width, bbox.height, bbox.depth]
|
||||
global_size_bounded = np.max(np.vstack((global_size, self.size_bounds[0, :])), axis=0)
|
||||
global_size_bounded = np.min(np.vstack((global_size_bounded, self.size_bounds[1, :])), axis=0)
|
||||
out_of_bounds = ~np.isclose(global_size_bounded, global_size)
|
||||
if not np.any(out_of_bounds):
|
||||
return
|
||||
|
||||
# Since we are working with the bbox, we have to transform on the global coordinate system.
|
||||
Logger.debug(f'The purge cube was scaled to avoid being larger than the buildplate.')
|
||||
scaling_factor = np.where(out_of_bounds, global_size_bounded / global_size, 1.0)
|
||||
self.scale(Vector(data=scaling_factor), transform_space=SceneNode.TransformSpace.World)
|
||||
|
||||
def _on_transform_forbid_xz_rotations(self, *args):
|
||||
orient = np.abs(self.getWorldOrientation().getData())
|
||||
if np.allclose(orient[[0, 2]], 0.0):
|
||||
return
|
||||
Logger.debug(f'Rotations of the purge cube not around the vertical Y-axis are forbidden. Reverting to previous state.')
|
||||
self.setOrientation(self.prev_orient, transform_space=SceneNode.TransformSpace.World)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def addDecorator(self, decorator: SceneNodeDecorator) -> None:
|
||||
""" Modified in order to circumvent the type check that does not allow subclasses """
|
||||
|
||||
if any(isinstance(dec, type(decorator)) for dec in self._decorators):
|
||||
Logger.log("w", "Unable to add the same decorator type (%s) to a SceneNode twice.", type(decorator))
|
||||
return
|
||||
try:
|
||||
decorator.setNode(self)
|
||||
except AttributeError:
|
||||
Logger.logException("e", "Unable to add decorator.")
|
||||
return
|
||||
self._decorators.append(decorator)
|
||||
self.decoratorsChanged.emit(self)
|
||||
|
||||
def getDecorator(self, dec_type: type[SceneNodeDecorator]) -> Optional[SceneNodeDecorator]:
|
||||
""" Modified in order to circumvent the type check that does not allow subclasses """
|
||||
|
||||
for decorator in self._decorators:
|
||||
if isinstance(decorator, dec_type):
|
||||
return decorator
|
||||
return None
|
||||
|
||||
def removeDecorator(self, dec_type: type[SceneNodeDecorator]) -> None:
|
||||
""" Modified in order to circumvent the type check that does not allow subclasses """
|
||||
|
||||
for decorator in self._decorators:
|
||||
if isinstance(decorator, dec_type):
|
||||
decorator.clear()
|
||||
self._decorators.remove(decorator)
|
||||
self.decoratorsChanged.emit(self)
|
||||
break
|
||||
|
||||
# def _on_node_addition(self, *args):
|
||||
# app = CuraApplication.getInstance()
|
||||
# if app is None:
|
||||
# return
|
||||
#
|
||||
# root_node = app.getController().getScene().getRoot()
|
||||
# children = root_node.getChildren()
|
||||
#
|
||||
# idx_first_node = 0
|
||||
# while not isinstance(children[idx_first_node], CuraSceneNode):
|
||||
# idx_first_node += 1
|
||||
# if idx_first_node >= len(children):
|
||||
# return
|
||||
# if children[idx_first_node] is self.node:
|
||||
# return
|
||||
#
|
||||
# idx_purge_cube = idx_first_node
|
||||
# while children[idx_purge_cube] is not self.node:
|
||||
# idx_purge_cube += 1
|
||||
# if idx_purge_cube >= len(children):
|
||||
# return
|
||||
#
|
||||
# # Move the purge cube node to the last position
|
||||
# root_node._children = [
|
||||
# *children[:idx_first_node],
|
||||
# self.node,
|
||||
# *children[idx_first_node:idx_purge_cube],
|
||||
# *children[idx_purge_cube + 1:],
|
||||
# ]
|
||||
# # root_node.childrenChanged.emit(root_node)
|
||||
#
|
||||
# def _enforce_print_order(self):
|
||||
# app = CuraApplication.getInstance()
|
||||
# if app is None or self.node is None:
|
||||
# return
|
||||
# if self.node.printOrder == 1:
|
||||
# return
|
||||
#
|
||||
# root_nodes = app.getController().getScene().getRoot()
|
||||
# printing_nodes = [child for child in root_nodes.getChildren() if isinstance(child, CuraSceneNode)]
|
||||
# for node in printing_nodes:
|
||||
# if node.printOrder < self.node.printOrder:
|
||||
# node.printOrder += 1
|
||||
# self.node.printOrder = 1
|
||||
|
||||
|
||||
|
||||
class PurgeCubeConvexHullDecorator(ConvexHullDecorator):
|
||||
def __init__(self, nozzle_height: float = 2.0):
|
||||
super().__init__()
|
||||
|
||||
self.nozzle_height = nozzle_height
|
||||
|
||||
def getNode(self) -> PurgeCubeSceneNode | None:
|
||||
return self._node
|
||||
|
||||
def _isSingularOneAtATimeNode(self) -> bool:
|
||||
""" Always printed as a whole, therefore a one-at-a-time node """
|
||||
return True
|
||||
|
||||
def _getHeadAndFans(self):
|
||||
""" If the z_height of the node is small, allow placing other models much much closer to the `PurgeCube` """
|
||||
if self.getNode().z_height > self.nozzle_height:
|
||||
return super()._getHeadAndFans()
|
||||
# To simulate the nozzle. Even at that height the nozzle can hit the purge cube
|
||||
return Polygon.approximatedCircle(8.0 / 2)
|
||||
|
||||
def _compute2DConvexHeadFull(self) -> Optional[Polygon]:
|
||||
convex_hull = self._compute2DConvexHull()
|
||||
convex_hull = self._add2DAdhesionMargin(convex_hull)
|
||||
if not convex_hull:
|
||||
return None
|
||||
head_hull = convex_hull.getMinkowskiHull(self._getHeadAndFans())
|
||||
if self._global_stack is not None \
|
||||
and self._global_stack.getProperty("print_sequence", "value") == "all_at_once":
|
||||
brim_hull = self._add2DAdhesionMargin(convex_hull)
|
||||
return head_hull.unionConvexHulls(brim_hull)
|
||||
return head_hull
|
||||
|
||||
|
||||
|
||||
def load_purge_cube(path: str, node_name: str = None) -> PurgeCubeSceneNode | None:
|
||||
""" Copy pasted from CuraApplication.readLocalFile
|
||||
|
||||
Removed unnecessary lines since we know that we are loading an stl
|
||||
"""
|
||||
file = QUrl(Path(__file__).parent.joinpath(path).as_uri())
|
||||
Logger.log("i", "Attempting to read file %s", file.toString())
|
||||
if not file.isValid():
|
||||
raise FileNotFoundError
|
||||
|
||||
if (app := CuraApplication.getInstance()) is None:
|
||||
return None
|
||||
|
||||
# No idea what isBlockSlicing decorator is, so I decided to just leave it as it is.
|
||||
scene = app.getController().getScene()
|
||||
for node in DepthFirstIterator(scene.getRoot()):
|
||||
if node.callDecoration("isBlockSlicing"):
|
||||
app.deleteAll()
|
||||
break
|
||||
|
||||
f = file.toLocalFile()
|
||||
app._currently_loading_files.append(f)
|
||||
# Using UM.FileHandler.ReadFileJob instead of UM.Mesh.ReadMeshJob, because the latter tries to be smart and
|
||||
# auto-scales the model if it is too small. This is annoying, so I am not using it.
|
||||
job = ReadFileJob(filename=f, handler=app.getMeshFileHandler(), add_to_recent_files=False)
|
||||
job.start()
|
||||
|
||||
# I do not really like callbacks that much. I wish cura used AsyncIO or at least Future or Promise, but no,
|
||||
# it uses callbacks. The way this is fine, since the model is just a cube with 8 vertices, so it is not very
|
||||
# computationally intensive (so it would not block the UI if this is the thread on which the UI runs).
|
||||
while not job.isFinished():
|
||||
if job.hasError():
|
||||
raise job.getError()
|
||||
time.sleep(0.1)
|
||||
|
||||
result = job.getResult()
|
||||
if not isinstance(result, typing.Sequence) or len(result) <= 0:
|
||||
raise RuntimeError('Did not load any meshes for some reason...')
|
||||
|
||||
# Code taken from CuraApplication._readMeshFinished where this code converts a SceneNode to
|
||||
# CuraSceneNode
|
||||
original_node = result[0]
|
||||
node = PurgeCubeSceneNode(name=node_name)
|
||||
node.setMeshData(original_node.getMeshData())
|
||||
node.source_mime_type = original_node.source_mime_type
|
||||
# Setting meshdata does not apply scaling.
|
||||
if original_node.getScale() != Vector(1.0, 1.0, 1.0):
|
||||
node.scale(original_node.getScale())
|
||||
|
||||
# Ensure that the correct decorator is attached to this new node.
|
||||
if node.getDecorator(ConvexHullDecorator):
|
||||
node.removeDecorator(ConvexHullDecorator)
|
||||
node.addDecorator(PurgeCubeConvexHullDecorator())
|
||||
|
||||
# Finish node creation using the default function
|
||||
job._result = [node]
|
||||
app._readMeshFinished(job)
|
||||
return node
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# def _on_model_finish_loading(self, filename: str):
|
||||
# """ Find the `CuraSceneNode` that contains the purge cube that was loaded by cura
|
||||
#
|
||||
# `CuraApplication.readLocalFile` loads the stl files in a separate worker thread. As a result, it does not
|
||||
# actually return the resulting node. So, after it finishes loading, we have to unironially find it. So,
|
||||
# all this method does is it finds to loaded node and saves its reference
|
||||
#
|
||||
# """
|
||||
#
|
||||
# if self.path != Path(filename):
|
||||
# return
|
||||
# app = CuraApplication.getInstance()
|
||||
# root_node = app.getController().getScene().getRoot()
|
||||
#
|
||||
# candidate_nodes = [n for n in root_node.getChildren() if n.getName() == self.path.name]
|
||||
# if len(candidate_nodes) == 0:
|
||||
# raise RuntimeError('No purge cube node was found!')
|
||||
# elif len(candidate_nodes) == 1:
|
||||
# self.node = candidate_nodes[0]
|
||||
# assert isinstance(self.node, CuraSceneNode)
|
||||
# else:
|
||||
# raise NotImplementedError('Likely multiple purge cubes created by multiple scripts. We are not dealing with that for now')
|
||||
#
|
||||
#
|
||||
# def _getNozzle(chd: ConvexHullDecorator) -> Polygon:
|
||||
# if not chd._global_stack:
|
||||
# return Polygon()
|
||||
#
|
||||
# polygon = Polygon.approximatedCircle(8.0/4)
|
||||
# return polygon
|
||||
#
|
||||
# def decorator(chd: ConvexHullDecorator, func):
|
||||
# def wrapper(*args, **kwargs):
|
||||
# return chd._compute2DConvexHeadFull()
|
||||
# return wrapper
|
||||
#
|
||||
# def decoratorMin(chd: ConvexHullDecorator, func):
|
||||
# def wrapper(*args, **kwargs):
|
||||
# if self.z_height > 2.0:
|
||||
# return func()
|
||||
#
|
||||
# return chd._compute2DConvexHeadFull()
|
||||
# return wrapper
|
||||
#
|
||||
# def decoratorHeadAndFans(chd: ConvexHullDecorator, func):
|
||||
# def wrapper(*args, **kwargs):
|
||||
# if self.z_height > 2.0:
|
||||
# return func()
|
||||
# polygon = Polygon.approximatedCircle(8.0/4)
|
||||
# offset_x = 0.0 # chd._getSettingProperty("machine_nozzle_offset_x", "value")
|
||||
# offset_y = 0.0 # chd._getSettingProperty("machine_nozzle_offset_y", "value")
|
||||
# return polygon.translate(-offset_x, -offset_y)
|
||||
# return wrapper
|
||||
#
|
||||
#
|
||||
# convex_hull_decor: ConvexHullDecorator | None = self.node.getDecorator(ConvexHullDecorator)
|
||||
# if convex_hull_decor is not None:
|
||||
# convex_hull_decor._isSingularOneAtATimeNode = lambda *args: True
|
||||
# convex_hull_decor._getHeadAndFans = decoratorHeadAndFans(convex_hull_decor, convex_hull_decor._getHeadAndFans)
|
||||
# convex_hull_decor.getConvexHullHeadFull = convex_hull_decor._compute2DConvexHeadFull
|
||||
# #convex_hull_decor._compute2DConvexHeadFull = decorator(convex_hull_decor, convex_hull_decor._compute2DConvexHeadFull)
|
||||
# #convex_hull_decor._compute2DConvexHeadMin = decoratorMin(convex_hull_decor, convex_hull_decor._compute2DConvexHeadMin)
|
||||
#
|
||||
# app.fileCompleted.disconnect(self._on_model_finish_loading) # No need to continue watching
|
||||
# self.node.transformationChanged.connect(self._on_transform)
|
||||
# root_node = app.getController().getScene().getRoot()
|
||||
# root_node.childrenChanged.connect(self._on_node_addition)
|
||||
# self._current_scale = self.node.getScale()
|
||||
#
|
||||
# if self.on_load is not None:
|
||||
# self.on_load()
|
||||
|
||||
|
143
plugins/PostProcessingPlugin/scripts/UseDryWiseDryer_readme.md
Normal file
143
plugins/PostProcessingPlugin/scripts/UseDryWiseDryer_readme.md
Normal file
|
@ -0,0 +1,143 @@
|
|||
|
||||
|
||||
# Motivation
|
||||
|
||||
This script is a highly requested feature from our clients. Normally, the dryer requires manual loading of the
|
||||
filament in the printer 40 mins after starting drying and manually starting the print 20 mins after loading.
|
||||
This means that the user must be nearby the dryer for up to 1 hour after starting drying. This is inconvenient because:
|
||||
1. It is distracting, especially when using multiple dryers.
|
||||
2. You cannot start the print last minute before you leave work.
|
||||
3. One may forget about the dryer, and as a result the model will not get printed at all.
|
||||
|
||||
This script attempts to resolve these issues by providing a more automated method of drying and printing, at
|
||||
the cost of ~2 g of purged wet filament (depending of the printer and dryer setup)
|
||||
|
||||
## How the script works
|
||||
|
||||
1. The wet filament is passed through the dryer and is attached to the extruder. Notice that the filament is **not** loaded in the printer, but the end of the filament is gripped by the extruder gear.
|
||||
2. The dryer is switched on and put in the printing mode, the print file is selected and the printer starts printing immediately.
|
||||
3. The script loads the filament in the printer, meaning that the printer moves the filament until it reaches the
|
||||
nozzle. The loading is done roughly at the printing speed defined by the slicer to ensure that the filament is
|
||||
dried uniformly.
|
||||
4. The loaded filament is wet, because it was not processed by the dryer. This filament now needs to be purged, so
|
||||
a purge cube is printed at the edge of the buildplate. The purge cube is printed until all unprocessed material is
|
||||
removed.
|
||||
5. The loaded filament is now dry. The printer begins printing the sliced model using the dried material. At the
|
||||
same time, the dryer is continuing drying the material.
|
||||
|
||||
|
||||
## How to install the script
|
||||
|
||||
It is expected that in the future versions no installation will be necessary as cura will integrate this script
|
||||
with the `PostProcessingPlugin` and make it available for all clients. Until then, the script has to be installed
|
||||
according to the following procedure:
|
||||
1. Download `UseDrywiseDryer.py`. Any other files in the repository are not required for proper functioning of the
|
||||
script.
|
||||
2. Locate your cura installation folder. Normally, it is installed in
|
||||
`C:\Program Files\UltiMaker Cura {version_number}`.
|
||||
3. In the cura directory find the folder `~\share\cura\plugins\PostProcessingPlugin\scripts` and put the script file(s) there.
|
||||
4. Restart cura (if already running). You are now ready to use the script.
|
||||
|
||||
## How to use the script
|
||||
|
||||
### Slicing
|
||||
1. Launch cura.
|
||||
2. Add your models and choose your printing profile
|
||||
2. Go to `Extensions/Post Processing/Modify G-code`.
|
||||
3. Click `Add a script` button and select `Use Drywise Dryer`. It is usually located at the bottom of all scripts.
|
||||
4. Enter the required parameters. For the printer that we have tested, the "Distance from extruder to nozzle"
|
||||
parameter is already pre-defined. However, the "Distance from dryer output to extruder" depends on your set-up,
|
||||
you will need to work it out yourself. See [Explanation of script parameters] section on the explanation of
|
||||
individual parameters as well as how to choose them.
|
||||
5. Close the script window and slice the model.
|
||||
6. Unforturnately, for now the purging cube is not shown in the "Preview". This is one of the current limitations
|
||||
on the script that will be addressed soon. However, you will be able to see the purging cube when viewing the gcode
|
||||
directly. Save the gcode and drag and drop it in the cura application.
|
||||
7. Ensure that the purge cube does not overlap with your model. If it does, then you will need to move your models
|
||||
away from the location where the purging cube will be printed and reslice the file again.
|
||||
8. You are now ready to print your model.
|
||||
|
||||
### Drying and printing
|
||||
1. Start the dryer and select the material to be dried.
|
||||
2. Press "Start" to move to the filament loading screen. Sharpen the filament and pass it through the dryer,
|
||||
like during normal operation. When you have done so, press "Next" on the dryer to finish loading procedure.
|
||||
2. Pull the filament out of the dryer and place it inside the extruder s.t. the extruder gear grips it firmly.
|
||||
Ensure that the extruder managed to grip the filament. You might want to test this by trying to pull
|
||||
the filament out of the extruder gear. **Note: do not load the filament all the way to the nozzle.
|
||||
If you do so, you may damage your printer. The printer will load the filament automatically during operation.**
|
||||
3. Position the dryer close to the printer, like during normal operation.
|
||||
4. Due to the way the script works, you will need to enter the continuous cycle immediately. At the moment, you
|
||||
should be at the "Pre-Drying Cycle" screen with the following buttons available: "Start", "Abort", and "Skip".
|
||||
To enter the continuous cycle immediately press the following buttons in the following sequence:
|
||||
> "Skip" > "Skip Pre-Drying" > "Skip" > "Skip Loading" > "Skip" > "Skip Preparing" > "Print started".
|
||||
5. You should now be at the printing screen, which you normally see when the printer is printing and the dryer
|
||||
is drying the filament as it is being extruded by the printer.
|
||||
5. Select your print file on your printer and start printing normally.
|
||||
6. You may return to whatever you are working on.
|
||||
|
||||
## Explanation of script parameters
|
||||
- **Extruder in Use**: Which extruder is connected to the dryer
|
||||
- **Distance from extruder to nozzle**: Self-explanatory. Notice that this value is usually constant among printers
|
||||
of the same model. As a result, we provide some preset values for the printers that we have tested.
|
||||
- **Distance from dryer output to extruder**: Self-explanatory. This value is highly dependent on your printer-drywise
|
||||
set up, therefore we leave it up to you to measure it.
|
||||
- **Position of the purge cube**: Allows you to choose on which side of the buildplate the purge cube is printed.
|
||||
- **Cube height**: By default, the cube height is 1.5 mm. Please, review the Issue #1, which describes our motivations
|
||||
for why it was not made any larger.
|
||||
|
||||
To measure the `Distance from extruder to nozzle` parameter, load the filament into the printer, mark the location of
|
||||
the extruder gear as close as you can, extract the filament and measure the distance between the filament tip
|
||||
|
||||
|
||||
# Current limitations
|
||||
- The purge cube does not appear in the "Prepare" and "Preview" tabs
|
||||
- There is no check for whether the model is printed too close to the purge cube
|
||||
- Two separate gcodes need to be sliced (one with the script and one without).
|
||||
- Wet filament has to be purged. This results in wasted material and money. However:
|
||||
1. Your time and attention may be worth more than purged material. After all, this is why you
|
||||
bought Drywise - to stop worrying about wet filament and focus on important work.
|
||||
2. The amount of purged material is constant regardless of the length of the print. Consequently,
|
||||
the larger the print, the smaller relative amount of material is wasted. As an example, for Ultimaker printers and
|
||||
Ender S1 Pro, the amount of material purged is ~2.0 g, therefore when printing a somewhat large model weighing 70 g,
|
||||
only 2.8% of the printed material is wasted.
|
||||
3. After printing the filament inside the printer and the dryer stay dry for some time. As a
|
||||
result, you may continue printing later in the day without purging any more material. However, after **X** hours
|
||||
we cannot guarantee the dryness of the material
|
||||
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
### Material leaking out of the nozzle during loading
|
||||
It appears that you have overestimated the loading length of the filament. The solution is to reduce
|
||||
the value of the **Distance from extruder to nozzle** parameter.
|
||||
|
||||
### First layer of the purge cube looks terrible / The purge cube got detached
|
||||
This could be caused by **Distance from extruder to nozzle** being too low, so when the printer starts
|
||||
purging the wet material, the filament is not fully loaded. You may try to increase the value of
|
||||
this parameter.
|
||||
|
||||
Note that bed adhesion is a common 3d printing problem, so the script may not be at fault. Poor bed adhesion
|
||||
may also be caused by:
|
||||
- Miscalibrated bed leveling and/or z offset.
|
||||
- Absense of adhesive on the buildplate
|
||||
|
||||
### Filament is ground in the extruder
|
||||
Although unlikely, this might be caused by the script where the **Distance from extruder to nozzle** parameter
|
||||
is grossly overestimated. However, it is more likely that the problem is caused by:
|
||||
- A clog in the nozzle
|
||||
- Too low nozzle temperature during printing
|
||||
- Extruder tension too small
|
||||
|
||||
### Material still wet even after the purge cube
|
||||
If the bottom of the model is still wet, while the top is dry, then you did not purge enough wet material.
|
||||
To fix this problem, increase **Distance from dryer output to extruder** accordingly.
|
||||
If the whole model is still wet, then there may be a problem with your Drywise. Please, contact our support
|
||||
team at support@drywise.co
|
||||
|
||||
### The dryer shows the "Filament stopped moving" message during loading
|
||||
The "Filament stopped moving" means that the dryer sensed that the filament moved <5cm in the last 5 mins.
|
||||
A possible simple solution for this problem is to check if the extruder is actually gripping the filament.
|
||||
If not, then check if the filament is ground. If yes, review the "Filament is ground in the extruder" section.
|
||||
|
||||
Otherwise, the cause of this problem may be complex, therefore, please, contact our support
|
||||
team at support@drywise.co
|
1382
plugins/PostProcessingPlugin/scripts/UseDrywiseDryer.py
Normal file
1382
plugins/PostProcessingPlugin/scripts/UseDrywiseDryer.py
Normal file
File diff suppressed because it is too large
Load diff
BIN
plugins/PostProcessingPlugin/scripts/cube.stl
Normal file
BIN
plugins/PostProcessingPlugin/scripts/cube.stl
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue