Merge branch 'master' into feature_sync_button

This commit is contained in:
Diego Prado Gesto 2018-03-08 16:57:21 +01:00
commit b212781a19
9 changed files with 396 additions and 25 deletions

4
.dockerignore Normal file
View file

@ -0,0 +1,4 @@
.git
.github
resources/materials
CuraEngine

45
Dockerfile Normal file
View file

@ -0,0 +1,45 @@
FROM ultimaker/cura-build-environment:1
# Environment vars for easy configuration
ENV CURA_APP_DIR=/srv/cura
# Ensure our sources dir exists
RUN mkdir $CURA_APP_DIR
# Setup CuraEngine
ENV CURA_ENGINE_BRANCH=master
WORKDIR $CURA_APP_DIR
RUN git clone -b $CURA_ENGINE_BRANCH --depth 1 https://github.com/Ultimaker/CuraEngine
WORKDIR $CURA_APP_DIR/CuraEngine
RUN mkdir build
WORKDIR $CURA_APP_DIR/CuraEngine/build
RUN cmake3 ..
RUN make
RUN make install
# TODO: setup libCharon
# Setup Uranium
ENV URANIUM_BRANCH=master
WORKDIR $CURA_APP_DIR
RUN git clone -b $URANIUM_BRANCH --depth 1 https://github.com/Ultimaker/Uranium
# Setup materials
ENV MATERIALS_BRANCH=master
WORKDIR $CURA_APP_DIR
RUN git clone -b $MATERIALS_BRANCH --depth 1 https://github.com/Ultimaker/fdm_materials materials
# Setup Cura
WORKDIR $CURA_APP_DIR/Cura
ADD . .
RUN mv $CURA_APP_DIR/materials resources/materials
# Make sure Cura can find CuraEngine
RUN ln -s /usr/local/bin/CuraEngine $CURA_APP_DIR/Cura
# Run Cura
WORKDIR $CURA_APP_DIR/Cura
ENV PYTHONPATH=${PYTHONPATH}:$CURA_APP_DIR/Uranium
RUN chmod +x ./CuraEngine
RUN chmod +x ./run_in_docker.sh
CMD "./run_in_docker.sh"

View file

@ -108,7 +108,7 @@ class MaterialManager(QObject):
Logger.log("e", "Missing root material node for [%s]. Probably caused by update using incomplete data." Logger.log("e", "Missing root material node for [%s]. Probably caused by update using incomplete data."
" Check all related signals for further debugging.", " Check all related signals for further debugging.",
material_group.name) material_group.name)
# Do nothing here, we wait for a next signal to trigger an update. self._update_timer.start()
return return
guid = material_group.root_material_node.metadata["GUID"] guid = material_group.root_material_node.metadata["GUID"]
self._guid_material_groups_map[guid].append(material_group) self._guid_material_groups_map[guid].append(material_group)
@ -218,6 +218,7 @@ class MaterialManager(QObject):
self.materialsUpdated.emit() self.materialsUpdated.emit()
def _updateMaps(self): def _updateMaps(self):
Logger.log("i", "Updating material lookup data ...")
self.initialize() self.initialize()
def _onContainerMetadataChanged(self, container): def _onContainerMetadataChanged(self, container):
@ -400,6 +401,16 @@ class MaterialManager(QObject):
material_diameter, root_material_id) material_diameter, root_material_id)
return node return node
def removeMaterialByRootId(self, root_material_id: str):
material_group = self.getMaterialGroup(root_material_id)
if not material_group:
Logger.log("i", "Unable to remove the material with id %s, because it doesn't exist.", root_material_id)
return
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
for node in nodes_to_remove:
self._container_registry.removeContainer(node.metadata["id"])
# #
# Methods for GUI # Methods for GUI
# #
@ -423,14 +434,7 @@ class MaterialManager(QObject):
@pyqtSlot("QVariant") @pyqtSlot("QVariant")
def removeMaterial(self, material_node: "MaterialNode"): def removeMaterial(self, material_node: "MaterialNode"):
root_material_id = material_node.metadata["base_file"] root_material_id = material_node.metadata["base_file"]
material_group = self.getMaterialGroup(root_material_id) self.removeMaterialByRootId(root_material_id)
if not material_group:
Logger.log("d", "Unable to remove the material with id %s, because it doesn't exist.", root_material_id)
return
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
for node in nodes_to_remove:
self._container_registry.removeContainer(node.metadata["id"])
# #
# Creates a duplicate of a material, which has the same GUID and base_file metadata. # Creates a duplicate of a material, which has the same GUID and base_file metadata.

View file

@ -389,8 +389,6 @@ class ContainerManager(QObject):
return ContainerManager.getInstance() return ContainerManager.getInstance()
def _performMerge(self, merge_into, merge, clear_settings = True): def _performMerge(self, merge_into, merge, clear_settings = True):
assert isinstance(merge, type(merge_into))
if merge == merge_into: if merge == merge_into:
return return

View file

@ -352,6 +352,14 @@ class MachineManager(QObject):
self.__emitChangedSignals() self.__emitChangedSignals()
@staticmethod
def getMachine(definition_id: str) -> Optional["GlobalStack"]:
machines = ContainerRegistry.getInstance().findContainerStacks(type = "machine")
for machine in machines:
if machine.definition.getId() == definition_id:
return machine
return None
@pyqtSlot(str, str) @pyqtSlot(str, str)
def addMachine(self, name: str, definition_id: str) -> None: def addMachine(self, name: str, definition_id: str) -> None:
new_stack = CuraStackBuilder.createMachine(name, definition_id) new_stack = CuraStackBuilder.createMachine(name, definition_id)

View file

@ -97,6 +97,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# In Cura 2.5 and 2.6, the empty profiles used to have those long names # In Cura 2.5 and 2.6, the empty profiles used to have those long names
self._old_empty_profile_id_dict = {"empty_%s" % k: "empty" for k in ["material", "variant"]} self._old_empty_profile_id_dict = {"empty_%s" % k: "empty" for k in ["material", "variant"]}
self._old_new_materials = {}
self._materials_to_select = {}
def _clearState(self):
self._id_mapping = {}
self._old_new_materials = {}
self._materials_to_select = {}
## Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results. ## Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results.
# This has nothing to do with speed, but with getting consistent new naming for instances & objects. # This has nothing to do with speed, but with getting consistent new naming for instances & objects.
def getNewId(self, old_id): def getNewId(self, old_id):
@ -449,10 +457,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# To avoid this, we postpone all signals so they don't get emitted immediately. But, please also be aware that, # To avoid this, we postpone all signals so they don't get emitted immediately. But, please also be aware that,
# because of this, do not expect to have the latest data in the lookup tables in project loading. # because of this, do not expect to have the latest data in the lookup tables in project loading.
# #
with postponeSignals(*signals, compress = CompressTechnique.CompressSingle): with postponeSignals(*signals, compress = CompressTechnique.NoCompression):
return self._read(file_name) return self._read(file_name)
def _read(self, file_name): def _read(self, file_name):
application = CuraApplication.getInstance()
material_manager = application.getMaterialManager()
self._clearState()
archive = zipfile.ZipFile(file_name, "r") archive = zipfile.ZipFile(file_name, "r")
cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")]
@ -545,31 +558,41 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if self._material_container_suffix is None: if self._material_container_suffix is None:
self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0] self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0]
if xml_material_profile: if xml_material_profile:
to_deserialize_material = False
material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)] material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
for material_container_file in material_container_files: for material_container_file in material_container_files:
container_id = self._stripFileToId(material_container_file) container_id = self._stripFileToId(material_container_file)
need_new_name = False
materials = self._container_registry.findInstanceContainers(id = container_id) materials = self._container_registry.findInstanceContainers(id = container_id)
if not materials: if not materials:
material_container = xml_material_profile(container_id) # No material found, deserialize this material later and add it
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"), to_deserialize_material = True
file_name = material_container_file)
containers_to_add.append(material_container)
else: else:
material_container = materials[0] material_container = materials[0]
old_material_root_id = material_container.getMetaDataEntry("base_file")
if not self._container_registry.isReadOnly(container_id): # Only create new materials if they are not read only. if not self._container_registry.isReadOnly(container_id): # Only create new materials if they are not read only.
to_deserialize_material = True
if self._resolve_strategies["material"] == "override": if self._resolve_strategies["material"] == "override":
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"), # Remove the old materials and then deserialize the one from the project
file_name = material_container_file) root_material_id = material_container.getMetaDataEntry("base_file")
material_manager.removeMaterialByRootId(root_material_id)
elif self._resolve_strategies["material"] == "new": elif self._resolve_strategies["material"] == "new":
# Note that we *must* deserialize it with a new ID, as multiple containers will be # Note that we *must* deserialize it with a new ID, as multiple containers will be
# auto created & added. # auto created & added.
material_container = xml_material_profile(self.getNewId(container_id)) container_id = self.getNewId(container_id)
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"), self._old_new_materials[old_material_root_id] = container_id
file_name = material_container_file) need_new_name = True
containers_to_add.append(material_container)
material_containers.append(material_container) if to_deserialize_material:
material_container = xml_material_profile(container_id)
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"),
file_name = container_id + "." + self._material_container_suffix)
if need_new_name:
new_name = ContainerRegistry.getInstance().uniqueName(material_container.getName())
material_container.setName(new_name)
containers_to_add.append(material_container)
Job.yieldThread() Job.yieldThread()
Logger.log("d", "Workspace loading is checking instance containers...") Logger.log("d", "Workspace loading is checking instance containers...")
@ -1081,11 +1104,18 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
old_to_new_material_dict[old_id] = each_material old_to_new_material_dict[old_id] = each_material
break break
global_stack.quality = empty_quality_container
# replace old material in global and extruder stacks with new # replace old material in global and extruder stacks with new
self._replaceStackMaterialWithNew(global_stack, old_to_new_material_dict) self._replaceStackMaterialWithNew(global_stack, old_to_new_material_dict)
if extruder_stacks: if extruder_stacks:
for each_extruder_stack in extruder_stacks: for extruder_stack in extruder_stacks:
self._replaceStackMaterialWithNew(each_extruder_stack, old_to_new_material_dict) if extruder_stack.material.getId() in ("empty", "empty_material"):
continue
old_root_material_id = extruder_stack.material.getMetaDataEntry("base_file")
if old_root_material_id in self._old_new_materials:
new_root_material_id = self._old_new_materials[old_root_material_id]
self._materials_to_select[extruder_stack.getMetaDataEntry("position")] = new_root_material_id
if extruder_stacks: if extruder_stacks:
for stack in extruder_stacks: for stack in extruder_stacks:
@ -1123,6 +1153,22 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
def _updateActiveMachine(self, global_stack): def _updateActiveMachine(self, global_stack):
# Actually change the active machine. # Actually change the active machine.
machine_manager = Application.getInstance().getMachineManager() machine_manager = Application.getInstance().getMachineManager()
material_manager = Application.getInstance().getMaterialManager()
# Switch materials if new materials are created due to conflicts
# We do it here because MaterialManager hasn't been updated in _read() yet.
for position, root_material_id in self._materials_to_select.items():
extruder_stack = global_stack.extruders[position]
material_diameter = extruder_stack.materialDiameter
material_node = material_manager.getMaterialNode(global_stack.getMetaDataEntry("definition"),
extruder_stack.variant.getName(),
material_diameter, root_material_id)
if material_node is None:
Application.getInstance().callLater(self._updateActiveMachine, global_stack)
return
extruder_stack.material = material_node.getContainer()
Logger.log("d", "Changed extruder [%s] to material [%s]", position, root_material_id)
machine_manager.setActiveMachine(global_stack.getId()) machine_manager.setActiveMachine(global_stack.getId())
# Notify everything/one that is to notify about changes. # Notify everything/one that is to notify about changes.

View file

@ -33,6 +33,9 @@ from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
class CuraEngineBackend(QObject, Backend): class CuraEngineBackend(QObject, Backend):
backendError = Signal()
## Starts the back-end plug-in. ## Starts the back-end plug-in.
# #
# This registers all the signal listeners and prepares for communication # This registers all the signal listeners and prepares for communication
@ -289,6 +292,7 @@ class CuraEngineBackend(QObject, Backend):
if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error: if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error:
self.backendStateChange.emit(BackendState.Error) self.backendStateChange.emit(BackendState.Error)
self.backendError.emit(job)
return return
if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible: if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible:
@ -297,6 +301,7 @@ class CuraEngineBackend(QObject, Backend):
"Unable to slice with the current material as it is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Unable to slice")) "Unable to slice with the current material as it is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show() self._error_message.show()
self.backendStateChange.emit(BackendState.Error) self.backendStateChange.emit(BackendState.Error)
self.backendError.emit(job)
else: else:
self.backendStateChange.emit(BackendState.NotStarted) self.backendStateChange.emit(BackendState.NotStarted)
return return
@ -325,6 +330,7 @@ class CuraEngineBackend(QObject, Backend):
title = catalog.i18nc("@info:title", "Unable to slice")) title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show() self._error_message.show()
self.backendStateChange.emit(BackendState.Error) self.backendStateChange.emit(BackendState.Error)
self.backendError.emit(job)
else: else:
self.backendStateChange.emit(BackendState.NotStarted) self.backendStateChange.emit(BackendState.NotStarted)
return return
@ -347,6 +353,7 @@ class CuraEngineBackend(QObject, Backend):
title = catalog.i18nc("@info:title", "Unable to slice")) title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show() self._error_message.show()
self.backendStateChange.emit(BackendState.Error) self.backendStateChange.emit(BackendState.Error)
self.backendError.emit(job)
return return
if job.getResult() == StartSliceJob.StartJobResult.BuildPlateError: if job.getResult() == StartSliceJob.StartJobResult.BuildPlateError:
@ -355,6 +362,7 @@ class CuraEngineBackend(QObject, Backend):
title = catalog.i18nc("@info:title", "Unable to slice")) title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show() self._error_message.show()
self.backendStateChange.emit(BackendState.Error) self.backendStateChange.emit(BackendState.Error)
self.backendError.emit(job)
else: else:
self.backendStateChange.emit(BackendState.NotStarted) self.backendStateChange.emit(BackendState.NotStarted)
@ -364,6 +372,7 @@ class CuraEngineBackend(QObject, Backend):
title = catalog.i18nc("@info:title", "Unable to slice")) title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show() self._error_message.show()
self.backendStateChange.emit(BackendState.Error) self.backendStateChange.emit(BackendState.Error)
self.backendError.emit(job)
else: else:
self.backendStateChange.emit(BackendState.NotStarted) self.backendStateChange.emit(BackendState.NotStarted)
self._invokeSlice() self._invokeSlice()

View file

@ -6229,6 +6229,259 @@
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": false, "settable_per_extruder": false,
"settable_per_meshgroup": false "settable_per_meshgroup": false
},
"bridge_settings_enabled":
{
"label": "Enable Bridge Settings",
"description": "Detect bridges and modify print speed, flow and fan settings while bridges are printed.",
"type": "bool",
"default_value": false,
"value": "not support_enable and not support_tree_enable",
"settable_per_mesh": true,
"settable_per_extruder": false,
"settable_per_meshgroup": false
},
"bridge_wall_min_length":
{
"label": "Minimum Bridge Wall Length",
"description": "Unsupported walls shorter than this will be printed using the normal wall settings. Longer unsupported walls will be printed using the bridge wall settings.",
"unit": "mm",
"type": "float",
"minimum_value": "0",
"default_value": 5,
"enabled": "bridge_settings_enabled",
"settable_per_mesh": false,
"settable_per_extruder": true
},
"bridge_skin_support_threshold":
{
"label": "Bridge Skin Support Threshold",
"description": "If a skin region is supported for less than this percentage of its area, print it using the bridge settings. Otherwise it is printed using the normal skin settings.",
"unit": "%",
"default_value": 50,
"type": "float",
"minimum_value": "0",
"maximum_value": "100",
"enabled": "bridge_settings_enabled",
"settable_per_mesh": true
},
"bridge_wall_max_overhang":
{
"label": "Bridge Wall Max Overhang",
"description": "The maximum allowed width of the region of air below a wall line before the wall is printed using bridge settings. Expressed as a percentage of the wall line width. When the air gap is wider than this, the wall line is printed using the bridge settings. Otherwise, the wall line is printed using the normal settings. The lower the value, the more likely it is that overhung wall lines will be printed using bridge settings.",
"unit": "%",
"default_value": 100,
"type": "float",
"minimum_value": "0",
"maximum_value": "100",
"enabled": "bridge_settings_enabled",
"settable_per_mesh": true
},
"bridge_wall_coast":
{
"label": "Bridge Wall Coasting",
"description": "This controls the distance the extruder should coast immediately before a bridge wall begins. Coasting before the bridge starts can reduce the pressure in the nozzle and may produce a flatter bridge.",
"unit": "%",
"default_value": 100,
"type": "float",
"minimum_value": "0",
"maximum_value": "500",
"enabled": "bridge_settings_enabled",
"settable_per_mesh": false
},
"bridge_wall_speed":
{
"label": "Bridge Wall Speed",
"description": "The speed at which the bridge walls are printed.",
"unit": "mm/s",
"type": "float",
"minimum_value": "cool_min_speed",
"maximum_value": "math.sqrt(machine_max_feedrate_x ** 2 + machine_max_feedrate_y ** 2)",
"maximum_value_warning": "300",
"default_value": 15,
"value": "max(cool_min_speed, speed_wall_0 / 2)",
"enabled": "bridge_settings_enabled",
"settable_per_mesh": true
},
"bridge_wall_material_flow":
{
"label": "Bridge Wall Flow",
"description": "When printing bridge walls, the amount of material extruded is multiplied by this value.",
"unit": "%",
"default_value": 50,
"type": "float",
"minimum_value": "5",
"minimum_value_warning": "50",
"maximum_value_warning": "150",
"enabled": "bridge_settings_enabled",
"settable_per_mesh": true
},
"bridge_skin_speed":
{
"label": "Bridge Skin Speed",
"description": "The speed at which bridge skin regions are printed.",
"unit": "mm/s",
"type": "float",
"minimum_value": "cool_min_speed",
"maximum_value": "math.sqrt(machine_max_feedrate_x ** 2 + machine_max_feedrate_y ** 2)",
"maximum_value_warning": "300",
"default_value": 15,
"value": "max(cool_min_speed, speed_topbottom / 2)",
"enabled": "bridge_settings_enabled",
"settable_per_mesh": true
},
"bridge_skin_material_flow":
{
"label": "Bridge Skin Flow",
"description": "When printing bridge skin regions, the amount of material extruded is multiplied by this value.",
"unit": "%",
"default_value": 60,
"type": "float",
"minimum_value": "5",
"minimum_value_warning": "50",
"maximum_value_warning": "150",
"enabled": "bridge_settings_enabled",
"settable_per_mesh": true
},
"bridge_skin_density":
{
"label": "Bridge Skin Density",
"description": "The density of the bridge skin layer. Values less than 100 will increase the gaps between the skin lines.",
"unit": "%",
"default_value": 100,
"type": "float",
"minimum_value": "5",
"maximum_value": "100",
"minimum_value_warning": "20",
"enabled": "bridge_settings_enabled",
"settable_per_mesh": true
},
"bridge_fan_speed":
{
"label": "Bridge Fan Speed",
"description": "Percentage fan speed to use when printing bridge walls and skin.",
"unit": "%",
"minimum_value": "0",
"maximum_value": "100",
"default_value": 100,
"type": "float",
"enabled": "bridge_settings_enabled",
"settable_per_mesh": true
},
"bridge_enable_more_layers":
{
"label": "Bridge Has Multiple Layers",
"description": "If enabled, the second and third layers above the air are printed using the following settings. Otherwise, those layers are printed using the normal settings.",
"type": "bool",
"default_value": true,
"enabled": "bridge_settings_enabled",
"settable_per_mesh": true
},
"bridge_skin_speed_2":
{
"label": "Bridge Second Skin Speed",
"description": "Print speed to use when printing the second bridge skin layer.",
"unit": "mm/s",
"type": "float",
"minimum_value": "cool_min_speed",
"maximum_value": "math.sqrt(machine_max_feedrate_x ** 2 + machine_max_feedrate_y ** 2)",
"maximum_value_warning": "300",
"default_value": 25,
"value": "bridge_skin_speed",
"enabled": "bridge_settings_enabled and bridge_enable_more_layers",
"settable_per_mesh": true
},
"bridge_skin_material_flow_2":
{
"label": "Bridge Second Skin Flow",
"description": "When printing the second bridge skin layer, the amount of material extruded is multiplied by this value.",
"unit": "%",
"default_value": 100,
"type": "float",
"minimum_value": "5",
"maximum_value": "500",
"minimum_value_warning": "50",
"maximum_value_warning": "150",
"enabled": "bridge_settings_enabled and bridge_enable_more_layers",
"settable_per_mesh": true
},
"bridge_skin_density_2":
{
"label": "Bridge Second Skin Density",
"description": "The density of the second bridge skin layer. Values less than 100 will increase the gaps between the skin lines.",
"unit": "%",
"default_value": 75,
"type": "float",
"minimum_value": "5",
"maximum_value": "100",
"minimum_value_warning": "20",
"enabled": "bridge_settings_enabled and bridge_enable_more_layers",
"settable_per_mesh": true
},
"bridge_fan_speed_2":
{
"label": "Bridge Second Skin Fan Speed",
"description": "Percentage fan speed to use when printing the second bridge skin layer.",
"unit": "%",
"minimum_value": "0",
"maximum_value": "100",
"default_value": 0,
"type": "float",
"enabled": "bridge_settings_enabled and bridge_enable_more_layers",
"settable_per_mesh": true
},
"bridge_skin_speed_3":
{
"label": "Bridge Third Skin Speed",
"description": "Print speed to use when printing the third bridge skin layer.",
"unit": "mm/s",
"type": "float",
"minimum_value": "cool_min_speed",
"maximum_value": "math.sqrt(machine_max_feedrate_x ** 2 + machine_max_feedrate_y ** 2)",
"maximum_value_warning": "300",
"default_value": 15,
"value": "bridge_skin_speed",
"enabled": "bridge_settings_enabled and bridge_enable_more_layers",
"settable_per_mesh": true
},
"bridge_skin_material_flow_3":
{
"label": "Bridge Third Skin Flow",
"description": "When printing the third bridge skin layer, the amount of material extruded is multiplied by this value.",
"unit": "%",
"default_value": 110,
"type": "float",
"minimum_value": "5",
"maximum_value": "500",
"minimum_value_warning": "50",
"maximum_value_warning": "150",
"enabled": "bridge_settings_enabled and bridge_enable_more_layers",
"settable_per_mesh": true
},
"bridge_skin_density_3":
{
"label": "Bridge Third Skin Density",
"description": "The density of the third bridge skin layer. Values less than 100 will increase the gaps between the skin lines.",
"unit": "%",
"default_value": 80,
"type": "float",
"minimum_value": "5",
"maximum_value": "100",
"minimum_value_warning": "20",
"enabled": "bridge_settings_enabled and bridge_enable_more_layers",
"settable_per_mesh": true
},
"bridge_fan_speed_3":
{
"label": "Bridge Third Skin Fan Speed",
"description": "Percentage fan speed to use when printing the third bridge skin layer.",
"unit": "%",
"minimum_value": "0",
"maximum_value": "100",
"default_value": 0,
"type": "float",
"enabled": "bridge_settings_enabled and bridge_enable_more_layers",
"settable_per_mesh": true
} }
} }
}, },

4
run_in_docker.sh Normal file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env bash
Xvfb :1 -screen 0 1280x800x16 &
export DISPLAY=:1.0
python3 cura_app.py --headless