mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-05-08 06:09:02 +08:00
Merge branch 'master' into tests-for-um3networkplugin
This commit is contained in:
commit
951a21ead7
@ -3,7 +3,6 @@
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from cura.API.Interface.Settings import Settings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -23,9 +22,6 @@ if TYPE_CHECKING:
|
||||
|
||||
class Interface:
|
||||
|
||||
# For now we use the same API version to be consistent.
|
||||
VERSION = PluginRegistry.APIVersion
|
||||
|
||||
def __init__(self, application: "CuraApplication") -> None:
|
||||
# API methods specific to the settings portion of the UI
|
||||
self.settings = Settings(application)
|
||||
|
@ -4,7 +4,6 @@ from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtProperty
|
||||
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from cura.API.Backups import Backups
|
||||
from cura.API.Interface import Interface
|
||||
from cura.API.Account import Account
|
||||
@ -22,7 +21,6 @@ if TYPE_CHECKING:
|
||||
class CuraAPI(QObject):
|
||||
|
||||
# For now we use the same API version to be consistent.
|
||||
VERSION = PluginRegistry.APIVersion
|
||||
__instance = None # type: "CuraAPI"
|
||||
_application = None # type: CuraApplication
|
||||
|
||||
@ -62,4 +60,4 @@ class CuraAPI(QObject):
|
||||
|
||||
@property
|
||||
def interface(self) -> "Interface":
|
||||
return self._interface
|
||||
return self._interface
|
||||
|
@ -489,7 +489,9 @@ class BuildVolume(SceneNode):
|
||||
|
||||
def _updateRaftThickness(self):
|
||||
old_raft_thickness = self._raft_thickness
|
||||
self._adhesion_type = self._global_container_stack.getProperty("adhesion_type", "value")
|
||||
if self._global_container_stack.extruders:
|
||||
# This might be called before the extruder stacks have initialised, in which case getting the adhesion_type fails
|
||||
self._adhesion_type = self._global_container_stack.getProperty("adhesion_type", "value")
|
||||
self._raft_thickness = 0.0
|
||||
if self._adhesion_type == "raft":
|
||||
self._raft_thickness = (
|
||||
|
@ -134,7 +134,7 @@ except ImportError:
|
||||
CuraVersion = "master" # [CodeStyle: Reflecting imported value]
|
||||
CuraBuildType = ""
|
||||
CuraDebugMode = False
|
||||
CuraSDKVersion = ""
|
||||
CuraSDKVersion = "5.0.0"
|
||||
|
||||
|
||||
class CuraApplication(QtApplication):
|
||||
@ -164,6 +164,7 @@ class CuraApplication(QtApplication):
|
||||
super().__init__(name = "cura",
|
||||
app_display_name = CuraAppDisplayName,
|
||||
version = CuraVersion,
|
||||
api_version = CuraSDKVersion,
|
||||
buildtype = CuraBuildType,
|
||||
is_debug_mode = CuraDebugMode,
|
||||
tray_icon_name = "cura-icon-32.png",
|
||||
|
@ -16,7 +16,7 @@ from .QualityGroup import QualityGroup
|
||||
from .QualityNode import QualityNode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
from UM.Settings.Interfaces import DefinitionContainerInterface
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from .QualityChangesGroup import QualityChangesGroup
|
||||
from cura.CuraApplication import CuraApplication
|
||||
@ -538,7 +538,7 @@ class QualityManager(QObject):
|
||||
# Example: for an Ultimaker 3 Extended, it has "quality_definition = ultimaker3". This means Ultimaker 3 Extended
|
||||
# shares the same set of qualities profiles as Ultimaker 3.
|
||||
#
|
||||
def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainer",
|
||||
def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainerInterface",
|
||||
default_definition_id: str = "fdmprinter") -> str:
|
||||
machine_definition_id = default_definition_id
|
||||
if parseBool(machine_definition.getMetaDataEntry("has_machine_quality", False)):
|
||||
|
@ -272,7 +272,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored)
|
||||
|
||||
# Min head hull is used for the push free
|
||||
convex_hull = self._compute2DConvexHeadFull()
|
||||
convex_hull = self._compute2DConvexHull()
|
||||
if convex_hull:
|
||||
return convex_hull.getMinkowskiHull(head_and_fans)
|
||||
return None
|
||||
|
@ -419,13 +419,13 @@ class ContainerManager(QObject):
|
||||
self._container_name_filters[name_filter] = entry
|
||||
|
||||
## Import single profile, file_url does not have to end with curaprofile
|
||||
@pyqtSlot(QUrl, result="QVariantMap")
|
||||
def importProfile(self, file_url: QUrl):
|
||||
@pyqtSlot(QUrl, result = "QVariantMap")
|
||||
def importProfile(self, file_url: QUrl) -> Dict[str, str]:
|
||||
if not file_url.isValid():
|
||||
return
|
||||
return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)}
|
||||
path = file_url.toLocalFile()
|
||||
if not path:
|
||||
return
|
||||
return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)}
|
||||
return self._container_registry.importProfile(path)
|
||||
|
||||
@pyqtSlot(QObject, QUrl, str)
|
||||
|
@ -5,12 +5,12 @@ import os
|
||||
import re
|
||||
import configparser
|
||||
|
||||
from typing import cast, Optional
|
||||
|
||||
from typing import cast, Dict, Optional
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from UM.Decorators import override
|
||||
from UM.Settings.ContainerFormatError import ContainerFormatError
|
||||
from UM.Settings.Interfaces import ContainerInterface
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
@ -28,7 +28,7 @@ from . import GlobalStack
|
||||
|
||||
import cura.CuraApplication
|
||||
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
||||
from cura.ReaderWriters.ProfileReader import NoProfileException
|
||||
from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
@ -161,20 +161,20 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
|
||||
## Imports a profile from a file
|
||||
#
|
||||
# \param file_name \type{str} the full path and filename of the profile to import
|
||||
# \return \type{Dict} dict with a 'status' key containing the string 'ok' or 'error', and a 'message' key
|
||||
# containing a message for the user
|
||||
def importProfile(self, file_name):
|
||||
# \param file_name The full path and filename of the profile to import.
|
||||
# \return Dict with a 'status' key containing the string 'ok' or 'error',
|
||||
# and a 'message' key containing a message for the user.
|
||||
def importProfile(self, file_name: str) -> Dict[str, str]:
|
||||
Logger.log("d", "Attempting to import profile %s", file_name)
|
||||
if not file_name:
|
||||
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, "Invalid path")}
|
||||
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Failed to import profile from <filename>{0}</filename>: {1}", file_name, "Invalid path")}
|
||||
|
||||
plugin_registry = PluginRegistry.getInstance()
|
||||
extension = file_name.split(".")[-1]
|
||||
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return
|
||||
return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Can't import profile from <filename>{0}</filename> before a printer is added.", file_name)}
|
||||
|
||||
machine_extruders = []
|
||||
for position in sorted(global_stack.extruders):
|
||||
@ -183,7 +183,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
|
||||
if meta_data["profile_reader"][0]["extension"] != extension:
|
||||
continue
|
||||
profile_reader = plugin_registry.getPluginObject(plugin_id)
|
||||
profile_reader = cast(ProfileReader, plugin_registry.getPluginObject(plugin_id))
|
||||
try:
|
||||
profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
|
||||
except NoProfileException:
|
||||
@ -221,13 +221,13 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
# Make sure we have a profile_definition in the file:
|
||||
if profile_definition is None:
|
||||
break
|
||||
machine_definition = self.findDefinitionContainers(id = profile_definition)
|
||||
if not machine_definition:
|
||||
machine_definitions = self.findDefinitionContainers(id = profile_definition)
|
||||
if not machine_definitions:
|
||||
Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition)
|
||||
return {"status": "error",
|
||||
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name)
|
||||
}
|
||||
machine_definition = machine_definition[0]
|
||||
machine_definition = machine_definitions[0]
|
||||
|
||||
# Get the expected machine definition.
|
||||
# i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode...
|
||||
@ -274,11 +274,12 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
setting_value = global_profile.getProperty(qc_setting_key, "value")
|
||||
|
||||
setting_definition = global_stack.getSettingDefinition(qc_setting_key)
|
||||
new_instance = SettingInstance(setting_definition, profile)
|
||||
new_instance.setProperty("value", setting_value)
|
||||
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
||||
profile.addInstance(new_instance)
|
||||
profile.setDirty(True)
|
||||
if setting_definition is not None:
|
||||
new_instance = SettingInstance(setting_definition, profile)
|
||||
new_instance.setProperty("value", setting_value)
|
||||
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
||||
profile.addInstance(new_instance)
|
||||
profile.setDirty(True)
|
||||
|
||||
global_profile.removeInstance(qc_setting_key, postpone_emit=True)
|
||||
extruder_profiles.append(profile)
|
||||
@ -290,7 +291,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
for profile_index, profile in enumerate(profile_or_list):
|
||||
if profile_index == 0:
|
||||
# This is assumed to be the global profile
|
||||
profile_id = (global_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
|
||||
profile_id = (cast(ContainerInterface, global_stack.getBottom()).getId() + "_" + name_seed).lower().replace(" ", "_")
|
||||
|
||||
elif profile_index < len(machine_extruders) + 1:
|
||||
# This is assumed to be an extruder profile
|
||||
|
@ -5,6 +5,7 @@ from typing import Any, List, Optional, TYPE_CHECKING
|
||||
|
||||
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
from UM.Logger import Logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
@ -38,7 +39,11 @@ class CuraFormulaFunctions:
|
||||
extruder_position = int(machine_manager.defaultExtruderPosition)
|
||||
|
||||
global_stack = machine_manager.activeMachine
|
||||
extruder_stack = global_stack.extruders[str(extruder_position)]
|
||||
try:
|
||||
extruder_stack = global_stack.extruders[str(extruder_position)]
|
||||
except KeyError:
|
||||
Logger.log("w", "Value for %s of extruder %s was requested, but that extruder is not available" % (property_key, extruder_position))
|
||||
return None
|
||||
|
||||
value = extruder_stack.getRawProperty(property_key, "value", context = context)
|
||||
if isinstance(value, SettingFunction):
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
@ -29,6 +29,7 @@ class ChangeLog(Extension, QObject,):
|
||||
self._change_logs = None
|
||||
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
||||
Application.getInstance().getPreferences().addPreference("general/latest_version_changelog_shown", "2.0.0") #First version of CURA with uranium
|
||||
self.setMenuName(catalog.i18nc("@item:inmenu", "Changelog"))
|
||||
self.addMenuItem(catalog.i18nc("@item:inmenu", "Show Changelog"), self.showChangelog)
|
||||
|
||||
def getChangeLogs(self):
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"source_version": "15.04",
|
||||
"target_version": 3,
|
||||
"target_version": "4.5",
|
||||
|
||||
"translation": {
|
||||
"machine_nozzle_size": "nozzle_size",
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import configparser # For reading the legacy profile INI files.
|
||||
@ -6,6 +6,7 @@ import io
|
||||
import json # For reading the Dictionary of Doom.
|
||||
import math # For mathematical operations included in the Dictionary of Doom.
|
||||
import os.path # For concatenating the path to the plugin and the relative path to the Dictionary of Doom.
|
||||
from typing import Dict
|
||||
|
||||
from UM.Application import Application # To get the machine manager to create the new profile in.
|
||||
from UM.Logger import Logger # Logging errors.
|
||||
@ -33,10 +34,11 @@ class LegacyProfileReader(ProfileReader):
|
||||
# \param json The JSON file to load the default setting values from. This
|
||||
# should not be a URL but a pre-loaded JSON handle.
|
||||
# \return A dictionary of the default values of the legacy Cura version.
|
||||
def prepareDefaults(self, json):
|
||||
def prepareDefaults(self, json: Dict[str, Dict[str, str]]) -> Dict[str, str]:
|
||||
defaults = {}
|
||||
for key in json["defaults"]: # We have to copy over all defaults from the JSON handle to a normal dict.
|
||||
defaults[key] = json["defaults"][key]
|
||||
if "defaults" in json:
|
||||
for key in json["defaults"]: # We have to copy over all defaults from the JSON handle to a normal dict.
|
||||
defaults[key] = json["defaults"][key]
|
||||
return defaults
|
||||
|
||||
## Prepares the local variables that can be used in evaluation of computing
|
||||
@ -80,11 +82,10 @@ class LegacyProfileReader(ProfileReader):
|
||||
Logger.log("i", "Importing legacy profile from file " + file_name + ".")
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
profile_id = container_registry.uniqueName("Imported Legacy Profile")
|
||||
profile = InstanceContainer(profile_id) # Create an empty profile.
|
||||
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
input_parser = configparser.ConfigParser(interpolation = None)
|
||||
try:
|
||||
parser.read([file_name]) # Parse the INI file.
|
||||
input_parser.read([file_name]) # Parse the INI file.
|
||||
except Exception as e:
|
||||
Logger.log("e", "Unable to open legacy profile %s: %s", file_name, str(e))
|
||||
return None
|
||||
@ -92,7 +93,7 @@ class LegacyProfileReader(ProfileReader):
|
||||
# Legacy Cura saved the profile under the section "profile_N" where N is the ID of a machine, except when you export in which case it saves it in the section "profile".
|
||||
# Since importing multiple machine profiles is out of scope, just import the first section we find.
|
||||
section = ""
|
||||
for found_section in parser.sections():
|
||||
for found_section in input_parser.sections():
|
||||
if found_section.startswith("profile"):
|
||||
section = found_section
|
||||
break
|
||||
@ -110,15 +111,13 @@ class LegacyProfileReader(ProfileReader):
|
||||
return None
|
||||
|
||||
defaults = self.prepareDefaults(dict_of_doom)
|
||||
legacy_settings = self.prepareLocals(parser, section, defaults) #Gets the settings from the legacy profile.
|
||||
legacy_settings = self.prepareLocals(input_parser, section, defaults) #Gets the settings from the legacy profile.
|
||||
|
||||
#Check the target version in the Dictionary of Doom with this application version.
|
||||
if "target_version" not in dict_of_doom:
|
||||
Logger.log("e", "Dictionary of Doom has no target version. Is it the correct JSON file?")
|
||||
return None
|
||||
if InstanceContainer.Version != dict_of_doom["target_version"]:
|
||||
Logger.log("e", "Dictionary of Doom of legacy profile reader (version %s) is not in sync with the current instance container version (version %s)!", dict_of_doom["target_version"], str(InstanceContainer.Version))
|
||||
return None
|
||||
# Serialised format into version 4.5. Do NOT upgrade this, let the version upgrader handle it.
|
||||
output_parser = configparser.ConfigParser(interpolation = None)
|
||||
output_parser.add_section("general")
|
||||
output_parser.add_section("metadata")
|
||||
output_parser.add_section("values")
|
||||
|
||||
if "translation" not in dict_of_doom:
|
||||
Logger.log("e", "Dictionary of Doom has no translation. Is it the correct JSON file?")
|
||||
@ -127,7 +126,7 @@ class LegacyProfileReader(ProfileReader):
|
||||
quality_definition = current_printer_definition.getMetaDataEntry("quality_definition")
|
||||
if not quality_definition:
|
||||
quality_definition = current_printer_definition.getId()
|
||||
profile.setDefinition(quality_definition)
|
||||
output_parser["general"]["definition"] = quality_definition
|
||||
for new_setting in dict_of_doom["translation"]: # Evaluate all new settings that would get a value from the translations.
|
||||
old_setting_expression = dict_of_doom["translation"][new_setting]
|
||||
compiled = compile(old_setting_expression, new_setting, "eval")
|
||||
@ -140,37 +139,34 @@ class LegacyProfileReader(ProfileReader):
|
||||
definitions = current_printer_definition.findDefinitions(key = new_setting)
|
||||
if definitions:
|
||||
if new_value != value_using_defaults and definitions[0].default_value != new_value: # Not equal to the default in the new Cura OR the default in the legacy Cura.
|
||||
profile.setProperty(new_setting, "value", new_value) # Store the setting in the profile!
|
||||
output_parser["values"][new_setting] = str(new_value) # Store the setting in the profile!
|
||||
|
||||
if len(profile.getAllKeys()) == 0:
|
||||
if len(output_parser["values"]) == 0:
|
||||
Logger.log("i", "A legacy profile was imported but everything evaluates to the defaults, creating an empty profile.")
|
||||
|
||||
profile.setMetaDataEntry("type", "profile")
|
||||
# don't know what quality_type it is based on, so use "normal" by default
|
||||
profile.setMetaDataEntry("quality_type", "normal")
|
||||
profile.setName(profile_id)
|
||||
profile.setDirty(True)
|
||||
output_parser["general"]["version"] = "4"
|
||||
output_parser["general"]["name"] = profile_id
|
||||
output_parser["metadata"]["type"] = "quality_changes"
|
||||
output_parser["metadata"]["quality_type"] = "normal" # Don't know what quality_type it is based on, so use "normal" by default.
|
||||
output_parser["metadata"]["position"] = "0" # We only support single extrusion.
|
||||
output_parser["metadata"]["setting_version"] = "5" # What the dictionary of doom is made for.
|
||||
|
||||
#Serialise and deserialise in order to perform the version upgrade.
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
data = profile.serialize()
|
||||
parser.read_string(data)
|
||||
parser["general"]["version"] = "1"
|
||||
if parser.has_section("values"):
|
||||
parser["settings"] = parser["values"]
|
||||
del parser["values"]
|
||||
# Serialise in order to perform the version upgrade.
|
||||
stream = io.StringIO()
|
||||
parser.write(stream)
|
||||
output_parser.write(stream)
|
||||
data = stream.getvalue()
|
||||
profile.deserialize(data)
|
||||
|
||||
# The definition can get reset to fdmprinter during the deserialization's upgrade. Here we set the definition
|
||||
# again.
|
||||
profile.setDefinition(quality_definition)
|
||||
profile = InstanceContainer(profile_id)
|
||||
profile.deserialize(data) # Also performs the version upgrade.
|
||||
profile.setDirty(True)
|
||||
|
||||
#We need to return one extruder stack and one global stack.
|
||||
global_container_id = container_registry.uniqueName("Global Imported Legacy Profile")
|
||||
# We duplicate the extruder profile into the global stack.
|
||||
# This may introduce some settings that are global in the extruder stack and some settings that are per-extruder in the global stack.
|
||||
# We don't care about that. The engine will ignore them anyway.
|
||||
global_profile = profile.duplicate(new_id = global_container_id, new_name = profile_id) #Needs to have the same name as the extruder profile.
|
||||
del global_profile.getMetaData()["position"] # Has no position because it's global.
|
||||
global_profile.setDirty(True)
|
||||
|
||||
profile_definition = "fdmprinter"
|
||||
|
190
plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py
Normal file
190
plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py
Normal file
@ -0,0 +1,190 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import configparser # An input for some functions we're testing.
|
||||
import os.path # To find the integration test .ini files.
|
||||
import pytest # To register tests with.
|
||||
import unittest.mock # To mock the application, plug-in and container registry out.
|
||||
|
||||
import UM.Application # To mock the application out.
|
||||
import UM.PluginRegistry # To mock the plug-in registry out.
|
||||
import UM.Settings.ContainerRegistry # To mock the container registry out.
|
||||
import UM.Settings.InstanceContainer # To intercept the serialised data from the read() function.
|
||||
|
||||
import LegacyProfileReader as LegacyProfileReaderModule # To get the directory of the module.
|
||||
from LegacyProfileReader import LegacyProfileReader # The module we're testing.
|
||||
|
||||
@pytest.fixture
|
||||
def legacy_profile_reader():
|
||||
return LegacyProfileReader()
|
||||
|
||||
test_prepareDefaultsData = [
|
||||
{
|
||||
"defaults":
|
||||
{
|
||||
"foo": "bar"
|
||||
},
|
||||
"cheese": "delicious"
|
||||
},
|
||||
{
|
||||
"cat": "fluffy",
|
||||
"dog": "floofy"
|
||||
}
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize("input", test_prepareDefaultsData)
|
||||
def test_prepareDefaults(legacy_profile_reader, input):
|
||||
output = legacy_profile_reader.prepareDefaults(input)
|
||||
if "defaults" in input:
|
||||
assert input["defaults"] == output
|
||||
else:
|
||||
assert output == {}
|
||||
|
||||
test_prepareLocalsData = [
|
||||
( # Ordinary case.
|
||||
{ # Parser data.
|
||||
"profile":
|
||||
{
|
||||
"layer_height": "0.2",
|
||||
"infill_density": "30"
|
||||
}
|
||||
},
|
||||
{ # Defaults.
|
||||
"layer_height": "0.1",
|
||||
"infill_density": "20",
|
||||
"line_width": "0.4"
|
||||
}
|
||||
),
|
||||
( # Empty data.
|
||||
{ # Parser data.
|
||||
"profile":
|
||||
{
|
||||
}
|
||||
},
|
||||
{ # Defaults.
|
||||
}
|
||||
),
|
||||
( # All defaults.
|
||||
{ # Parser data.
|
||||
"profile":
|
||||
{
|
||||
}
|
||||
},
|
||||
{ # Defaults.
|
||||
"foo": "bar",
|
||||
"boo": "far"
|
||||
}
|
||||
),
|
||||
( # Multiple config sections.
|
||||
{ # Parser data.
|
||||
"some_other_name":
|
||||
{
|
||||
"foo": "bar"
|
||||
},
|
||||
"profile":
|
||||
{
|
||||
"foo": "baz" #Not the same as in some_other_name
|
||||
}
|
||||
},
|
||||
{ # Defaults.
|
||||
"foo": "bla"
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize("parser_data, defaults", test_prepareLocalsData)
|
||||
def test_prepareLocals(legacy_profile_reader, parser_data, defaults):
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read_dict(parser_data)
|
||||
|
||||
output = legacy_profile_reader.prepareLocals(parser, "profile", defaults)
|
||||
|
||||
assert set(defaults.keys()) <= set(output.keys()) # All defaults must be in there.
|
||||
assert set(parser_data["profile"]) <= set(output.keys()) # All overwritten values must be in there.
|
||||
for key in output:
|
||||
if key in parser_data["profile"]:
|
||||
assert output[key] == parser_data["profile"][key] # If overwritten, must be the overwritten value.
|
||||
else:
|
||||
assert output[key] == defaults[key] # Otherwise must be equal to the default.
|
||||
|
||||
test_prepareLocalsNoSectionErrorData = [
|
||||
( # Section does not exist.
|
||||
{ # Parser data.
|
||||
"some_other_name":
|
||||
{
|
||||
"foo": "bar"
|
||||
},
|
||||
},
|
||||
{ # Defaults.
|
||||
"foo": "baz"
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
## Test cases where a key error is expected.
|
||||
@pytest.mark.parametrize("parser_data, defaults", test_prepareLocalsNoSectionErrorData)
|
||||
def test_prepareLocalsNoSectionError(legacy_profile_reader, parser_data, defaults):
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read_dict(parser_data)
|
||||
|
||||
with pytest.raises(configparser.NoSectionError):
|
||||
legacy_profile_reader.prepareLocals(parser, "profile", defaults)
|
||||
|
||||
intercepted_data = ""
|
||||
|
||||
@pytest.mark.parametrize("file_name", ["normal_case.ini"])
|
||||
def test_read(legacy_profile_reader, file_name):
|
||||
# Mock out all dependencies. Quite a lot!
|
||||
global_stack = unittest.mock.MagicMock()
|
||||
global_stack.getProperty = unittest.mock.MagicMock(return_value = 1) # For machine_extruder_count setting.
|
||||
def getMetaDataEntry(key, default_value = ""):
|
||||
if key == "quality_definition":
|
||||
return "mocked_quality_definition"
|
||||
if key == "has_machine_quality":
|
||||
return "True"
|
||||
global_stack.definition.getMetaDataEntry = getMetaDataEntry
|
||||
global_stack.definition.getId = unittest.mock.MagicMock(return_value = "mocked_global_definition")
|
||||
application = unittest.mock.MagicMock()
|
||||
application.getGlobalContainerStack = unittest.mock.MagicMock(return_value = global_stack)
|
||||
application_getInstance = unittest.mock.MagicMock(return_value = application)
|
||||
container_registry = unittest.mock.MagicMock()
|
||||
container_registry_getInstance = unittest.mock.MagicMock(return_value = container_registry)
|
||||
container_registry.uniqueName = unittest.mock.MagicMock(return_value = "Imported Legacy Profile")
|
||||
container_registry.findDefinitionContainers = unittest.mock.MagicMock(return_value = [global_stack.definition])
|
||||
UM.Settings.InstanceContainer.setContainerRegistry(container_registry)
|
||||
plugin_registry = unittest.mock.MagicMock()
|
||||
plugin_registry_getInstance = unittest.mock.MagicMock(return_value = plugin_registry)
|
||||
plugin_registry.getPluginPath = unittest.mock.MagicMock(return_value = os.path.dirname(LegacyProfileReaderModule.__file__))
|
||||
|
||||
# Mock out the resulting InstanceContainer so that we can intercept the data before it's passed through the version upgrader.
|
||||
def deserialize(self, data): # Intercepts the serialised data that we'd perform the version upgrade from when deserializing.
|
||||
global intercepted_data
|
||||
intercepted_data = data
|
||||
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read_string(data)
|
||||
self._metadata["position"] = parser["metadata"]["position"]
|
||||
def duplicate(self, new_id, new_name):
|
||||
self._metadata["id"] = new_id
|
||||
self._metadata["name"] = new_name
|
||||
return self
|
||||
|
||||
with unittest.mock.patch.object(UM.Application.Application, "getInstance", application_getInstance):
|
||||
with unittest.mock.patch.object(UM.Settings.ContainerRegistry.ContainerRegistry, "getInstance", container_registry_getInstance):
|
||||
with unittest.mock.patch.object(UM.PluginRegistry.PluginRegistry, "getInstance", plugin_registry_getInstance):
|
||||
with unittest.mock.patch.object(UM.Settings.InstanceContainer.InstanceContainer, "deserialize", deserialize):
|
||||
with unittest.mock.patch.object(UM.Settings.InstanceContainer.InstanceContainer, "duplicate", duplicate):
|
||||
result = legacy_profile_reader.read(os.path.join(os.path.dirname(__file__), file_name))
|
||||
|
||||
assert len(result) == 1
|
||||
|
||||
# Let's see what's inside the actual output file that we generated.
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read_string(intercepted_data)
|
||||
assert parser["general"]["definition"] == "mocked_quality_definition"
|
||||
assert parser["general"]["version"] == "4" # Yes, before we upgraded.
|
||||
assert parser["general"]["name"] == "Imported Legacy Profile" # Because we overwrote uniqueName.
|
||||
assert parser["metadata"]["type"] == "quality_changes"
|
||||
assert parser["metadata"]["quality_type"] == "normal"
|
||||
assert parser["metadata"]["position"] == "0"
|
||||
assert parser["metadata"]["setting_version"] == "5" # Yes, before we upgraded.
|
7
plugins/LegacyProfileReader/tests/normal_case.ini
Normal file
7
plugins/LegacyProfileReader/tests/normal_case.ini
Normal file
@ -0,0 +1,7 @@
|
||||
[profile]
|
||||
foo = bar
|
||||
boo = far
|
||||
fill_overlap = 3
|
||||
|
||||
[alterations]
|
||||
some = values
|
@ -32,7 +32,8 @@ class PostProcessingPlugin(QObject, Extension):
|
||||
def __init__(self, parent = None) -> None:
|
||||
QObject.__init__(self, parent)
|
||||
Extension.__init__(self)
|
||||
self.addMenuItem(i18n_catalog.i18n("Modify G-Code"), self.showPopup)
|
||||
self.setMenuName(i18n_catalog.i18nc("@item:inmenu", "Post Processing"))
|
||||
self.addMenuItem(i18n_catalog.i18nc("@item:inmenu", "Modify G-Code"), self.showPopup)
|
||||
self._view = None
|
||||
|
||||
# Loaded scripts are all scripts that can be used
|
||||
|
@ -12,7 +12,7 @@ from UM.Qt.ListModel import ListModel
|
||||
from .ConfigsModel import ConfigsModel
|
||||
|
||||
|
||||
## Model that holds cura packages. By setting the filter property the instances held by this model can be changed.
|
||||
## Model that holds Cura packages. By setting the filter property the instances held by this model can be changed.
|
||||
class PackagesModel(ListModel):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
@ -70,7 +70,7 @@ class PackagesModel(ListModel):
|
||||
|
||||
# Links is a list of dictionaries with "title" and "url". Convert this list into a dict so it's easier
|
||||
# to process.
|
||||
link_list = package['data']['links'] if 'links' in package['data'] else []
|
||||
link_list = package["data"]["links"] if "links" in package["data"] else []
|
||||
links_dict = {d["title"]: d["url"] for d in link_list}
|
||||
|
||||
if "author_id" not in package["author"] or "display_name" not in package["author"]:
|
||||
|
@ -172,18 +172,18 @@ class Toolbox(QObject, Extension):
|
||||
self._cloud_api_version = self._getCloudAPIVersion()
|
||||
self._cloud_api_root = self._getCloudAPIRoot()
|
||||
self._api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format(
|
||||
cloud_api_root=self._cloud_api_root,
|
||||
cloud_api_version=self._cloud_api_version,
|
||||
sdk_version=self._sdk_version
|
||||
cloud_api_root = self._cloud_api_root,
|
||||
cloud_api_version = self._cloud_api_version,
|
||||
sdk_version = self._sdk_version
|
||||
)
|
||||
self._request_urls = {
|
||||
"authors": QUrl("{base_url}/authors".format(base_url=self._api_url)),
|
||||
"packages": QUrl("{base_url}/packages".format(base_url=self._api_url)),
|
||||
"plugins_showcase": QUrl("{base_url}/showcase".format(base_url=self._api_url)),
|
||||
"plugins_available": QUrl("{base_url}/packages?package_type=plugin".format(base_url=self._api_url)),
|
||||
"materials_showcase": QUrl("{base_url}/showcase".format(base_url=self._api_url)),
|
||||
"materials_available": QUrl("{base_url}/packages?package_type=material".format(base_url=self._api_url)),
|
||||
"materials_generic": QUrl("{base_url}/packages?package_type=material&tags=generic".format(base_url=self._api_url))
|
||||
"authors": QUrl("{base_url}/authors".format(base_url = self._api_url)),
|
||||
"packages": QUrl("{base_url}/packages".format(base_url = self._api_url)),
|
||||
"plugins_showcase": QUrl("{base_url}/showcase".format(base_url = self._api_url)),
|
||||
"plugins_available": QUrl("{base_url}/packages?package_type=plugin".format(base_url = self._api_url)),
|
||||
"materials_showcase": QUrl("{base_url}/showcase".format(base_url = self._api_url)),
|
||||
"materials_available": QUrl("{base_url}/packages?package_type=material".format(base_url = self._api_url)),
|
||||
"materials_generic": QUrl("{base_url}/packages?package_type=material&tags=generic".format(base_url = self._api_url))
|
||||
}
|
||||
|
||||
# Get the API root for the packages API depending on Cura version settings.
|
||||
@ -209,11 +209,11 @@ class Toolbox(QObject, Extension):
|
||||
# Get the packages version depending on Cura version settings.
|
||||
def _getSDKVersion(self) -> Union[int, str]:
|
||||
if not hasattr(cura, "CuraVersion"):
|
||||
return self._plugin_registry.APIVersion
|
||||
return self._application.getAPIVersion().getMajor()
|
||||
if not hasattr(cura.CuraVersion, "CuraSDKVersion"): # type: ignore
|
||||
return self._plugin_registry.APIVersion
|
||||
return self._application.getAPIVersion().getMajor()
|
||||
if not cura.CuraVersion.CuraSDKVersion: # type: ignore
|
||||
return self._plugin_registry.APIVersion
|
||||
return self._application.getAPIVersion().getMajor()
|
||||
return cura.CuraVersion.CuraSDKVersion # type: ignore
|
||||
|
||||
@pyqtSlot()
|
||||
@ -299,7 +299,7 @@ class Toolbox(QObject, Extension):
|
||||
for plugin_id in old_plugin_ids:
|
||||
# Neither the installed packages nor the packages that are scheduled to remove are old plugins
|
||||
if plugin_id not in installed_package_ids and plugin_id not in scheduled_to_remove_package_ids:
|
||||
Logger.log('i', 'Found a plugin that was installed with the old plugin browser: %s', plugin_id)
|
||||
Logger.log("i", "Found a plugin that was installed with the old plugin browser: %s", plugin_id)
|
||||
|
||||
old_metadata = self._plugin_registry.getMetaData(plugin_id)
|
||||
new_metadata = self._convertPluginMetadata(old_metadata)
|
||||
@ -511,7 +511,10 @@ class Toolbox(QObject, Extension):
|
||||
# version, we also need to check if the current one has a lower SDK version. If so, this package should also
|
||||
# be upgradable.
|
||||
elif remote_version == local_version:
|
||||
can_upgrade = local_package.get("sdk_version", 0) < remote_package.get("sdk_version", 0)
|
||||
# First read sdk_version_semver. If that doesn't exist, read just sdk_version (old version system).
|
||||
remote_sdk_version = Version(remote_package.get("sdk_version_semver", remote_package.get("sdk_version", 0)))
|
||||
local_sdk_version = Version(local_package.get("sdk_version_semver", local_package.get("sdk_version", 0)))
|
||||
can_upgrade = local_sdk_version < remote_sdk_version
|
||||
|
||||
return can_upgrade
|
||||
|
||||
|
@ -78,7 +78,7 @@
|
||||
"machine_extruder_start_code":
|
||||
{
|
||||
"label": "Extruder Start G-Code",
|
||||
"description": "Start g-code to execute whenever turning the extruder on.",
|
||||
"description": "Start g-code to execute when switching to this extruder.",
|
||||
"type": "str",
|
||||
"default_value": "",
|
||||
"settable_per_mesh": false,
|
||||
@ -124,7 +124,7 @@
|
||||
"machine_extruder_end_code":
|
||||
{
|
||||
"label": "Extruder End G-Code",
|
||||
"description": "End g-code to execute whenever turning the extruder off.",
|
||||
"description": "End g-code to execute when switching away from this extruder.",
|
||||
"type": "str",
|
||||
"default_value": "",
|
||||
"settable_per_mesh": false,
|
||||
|
@ -2406,7 +2406,7 @@
|
||||
"switch_extruder_retraction_amount":
|
||||
{
|
||||
"label": "Nozzle Switch Retraction Distance",
|
||||
"description": "The amount of retraction: Set at 0 for no retraction at all. This should generally be the same as the length of the heat zone.",
|
||||
"description": "The amount of retraction when switching extruders. Set to 0 for no retraction at all. This should generally be the same as the length of the heat zone.",
|
||||
"type": "float",
|
||||
"unit": "mm",
|
||||
"enabled": "retraction_enable",
|
||||
@ -4139,6 +4139,20 @@
|
||||
"limit_to_extruder": "support_infill_extruder_nr",
|
||||
"settable_per_mesh": false
|
||||
},
|
||||
"minimum_support_area":
|
||||
{
|
||||
"label": "Minimum Support Area",
|
||||
"description": "Minimum area size for support polygons. Polygons which have an area smaller than this value will not be generated.",
|
||||
"unit": "mm²",
|
||||
"type": "float",
|
||||
"default_value": 0.0,
|
||||
"minimum_value": "0",
|
||||
"enabled": "support_enable",
|
||||
"limit_to_extruder": "support_infill_extruder_nr",
|
||||
"settable_per_mesh": true,
|
||||
"fabricate_enabled": true,
|
||||
"intermediate_enabled": true
|
||||
},
|
||||
"support_interface_enable":
|
||||
{
|
||||
"label": "Enable Support Interface",
|
||||
@ -4378,6 +4392,50 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"minimum_interface_area":
|
||||
{
|
||||
"label": "Minimum Support Interface Area",
|
||||
"description": "Minimum area size for support interface polygons. Polygons which have an area smaller than this value will not be generated.",
|
||||
"unit": "mm²",
|
||||
"type": "float",
|
||||
"default_value": 1.0,
|
||||
"minimum_value": "0",
|
||||
"minimum_value_warning": "minimum_support_area",
|
||||
"limit_to_extruder": "support_interface_extruder_nr",
|
||||
"enabled": "support_interface_enable and support_enable",
|
||||
"settable_per_mesh": true,
|
||||
"children":
|
||||
{
|
||||
"minimum_roof_area":
|
||||
{
|
||||
"label": "Minimum Support Roof Area",
|
||||
"description": "Minimum area size for the roofs of the support. Polygons which have an area smaller than this value will not be generated.",
|
||||
"unit": "mm²",
|
||||
"type": "float",
|
||||
"default_value": 1.0,
|
||||
"value": "extruderValue(support_roof_extruder_nr, 'minimum_interface_area')",
|
||||
"minimum_value": "0",
|
||||
"minimum_value_warning": "minimum_support_area",
|
||||
"limit_to_extruder": "support_roof_extruder_nr",
|
||||
"enabled": "support_roof_enable and support_enable",
|
||||
"settable_per_mesh": true
|
||||
},
|
||||
"minimum_bottom_area":
|
||||
{
|
||||
"label": "Minimum Support Floor Area",
|
||||
"description": "Minimum area size for the floors of the support. Polygons which have an area smaller than this value will not be generated.",
|
||||
"unit": "mm²",
|
||||
"type": "float",
|
||||
"default_value": 1.0,
|
||||
"value": "extruderValue(support_bottom_extruder_nr, 'minimum_interface_area')",
|
||||
"minimum_value": "0",
|
||||
"minimum_value_warning": "minimum_support_area",
|
||||
"limit_to_extruder": "support_bottom_extruder_nr",
|
||||
"enabled": "support_bottom_enable and support_enable",
|
||||
"settable_per_mesh": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"support_interface_offset":
|
||||
{
|
||||
"label": "Support Interface Horizontal Expansion",
|
||||
@ -4398,7 +4456,6 @@
|
||||
"description": "Amount of offset applied to the roofs of the support.",
|
||||
"unit": "mm",
|
||||
"type": "float",
|
||||
"minimum_value": "0",
|
||||
"default_value": 0.0,
|
||||
"value": "extruderValue(support_roof_extruder_nr, 'support_interface_offset')",
|
||||
"maximum_value": "extruderValue(support_extruder_nr, 'support_offset')",
|
||||
@ -4413,7 +4470,6 @@
|
||||
"description": "Amount of offset applied to the floors of the support.",
|
||||
"unit": "mm",
|
||||
"type": "float",
|
||||
"minimum_value": "0",
|
||||
"default_value": 0.0,
|
||||
"value": "extruderValue(support_bottom_extruder_nr, 'support_interface_offset')",
|
||||
"maximum_value": "extruderValue(support_extruder_nr, 'support_offset')",
|
||||
|
@ -8,7 +8,7 @@ setting_version = 5
|
||||
type = quality
|
||||
quality_type = fast
|
||||
weight = -1
|
||||
material = fabtotum_abs
|
||||
material = generic_abs
|
||||
|
||||
[values]
|
||||
adhesion_type = raft
|
||||
|
@ -8,7 +8,7 @@ setting_version = 5
|
||||
type = quality
|
||||
quality_type = high
|
||||
weight = 1
|
||||
material = fabtotum_abs
|
||||
material = generic_abs
|
||||
|
||||
[values]
|
||||
adhesion_type = raft
|
||||
|
@ -8,7 +8,7 @@ setting_version = 5
|
||||
type = quality
|
||||
quality_type = normal
|
||||
weight = 0
|
||||
material = fabtotum_abs
|
||||
material = generic_abs
|
||||
|
||||
[values]
|
||||
adhesion_type = raft
|
||||
|
@ -8,7 +8,7 @@ setting_version = 5
|
||||
type = quality
|
||||
quality_type = fast
|
||||
weight = -1
|
||||
material = fabtotum_nylon
|
||||
material = generic_nylon
|
||||
|
||||
[values]
|
||||
adhesion_type = raft
|
||||
|
@ -8,7 +8,7 @@ setting_version = 5
|
||||
type = quality
|
||||
quality_type = high
|
||||
weight = 1
|
||||
material = fabtotum_nylon
|
||||
material = generic_nylon
|
||||
|
||||
[values]
|
||||
adhesion_type = raft
|
||||
|
@ -8,7 +8,7 @@ setting_version = 5
|
||||
type = quality
|
||||
quality_type = normal
|
||||
weight = 0
|
||||
material = fabtotum_nylon
|
||||
material = generic_nylon
|
||||
|
||||
[values]
|
||||
adhesion_type = raft
|
||||
|
@ -8,7 +8,7 @@ setting_version = 5
|
||||
type = quality
|
||||
quality_type = fast
|
||||
weight = -1
|
||||
material = fabtotum_pla
|
||||
material = generic_pla
|
||||
|
||||
[values]
|
||||
adhesion_type = skirt
|
||||
|
@ -8,7 +8,7 @@ setting_version = 5
|
||||
type = quality
|
||||
quality_type = high
|
||||
weight = 1
|
||||
material = fabtotum_pla
|
||||
material = generic_pla
|
||||
|
||||
[values]
|
||||
adhesion_type = skirt
|
||||
|
@ -8,7 +8,7 @@ setting_version = 5
|
||||
type = quality
|
||||
quality_type = normal
|
||||
weight = 0
|
||||
material = fabtotum_pla
|
||||
material = generic_pla
|
||||
|
||||
[values]
|
||||
adhesion_type = skirt
|
||||
|
@ -6,7 +6,7 @@ name = Fast Quality
|
||||
[metadata]
|
||||
type = quality
|
||||
setting_version = 5
|
||||
material = fabtotum_tpu
|
||||
material = generic_tpu
|
||||
quality_type = fast
|
||||
weight = -1
|
||||
|
||||
|
@ -6,7 +6,7 @@ name = High Quality
|
||||
[metadata]
|
||||
type = quality
|
||||
setting_version = 5
|
||||
material = fabtotum_tpu
|
||||
material = generic_tpu
|
||||
quality_type = high
|
||||
weight = 1
|
||||
|
||||
|
@ -6,7 +6,7 @@ name = Normal Quality
|
||||
[metadata]
|
||||
type = quality
|
||||
setting_version = 5
|
||||
material = fabtotum_TPU
|
||||
material = generic_tpu
|
||||
quality_type = normal
|
||||
weight = 0
|
||||
|
||||
|
@ -8,7 +8,7 @@ setting_version = 5
|
||||
type = quality
|
||||
quality_type = fast
|
||||
weight = 1
|
||||
material = zyyx_pro_flex
|
||||
material = generic_tpu
|
||||
|
||||
[values]
|
||||
layer_height = 0.3
|
||||
|
@ -8,7 +8,7 @@ setting_version = 5
|
||||
type = quality
|
||||
quality_type = fine
|
||||
weight = 3
|
||||
material = zyyx_pro_flex
|
||||
material = generic_tpu
|
||||
|
||||
[values]
|
||||
layer_height = 0.12
|
||||
|
@ -8,7 +8,7 @@ setting_version = 5
|
||||
type = quality
|
||||
quality_type = normal
|
||||
weight = 2
|
||||
material = zyyx_pro_flex
|
||||
material = generic_tpu
|
||||
|
||||
[values]
|
||||
layer_height = 0.2
|
||||
|
@ -8,7 +8,7 @@ setting_version = 5
|
||||
type = quality
|
||||
quality_type = fast
|
||||
weight = 1
|
||||
material = zyyx_pro_pla
|
||||
material = generic_pla
|
||||
|
||||
[values]
|
||||
layer_height = 0.3
|
||||
|
@ -8,7 +8,7 @@ setting_version = 5
|
||||
type = quality
|
||||
quality_type = fine
|
||||
weight = 3
|
||||
material = zyyx_pro_pla
|
||||
material = generic_pla
|
||||
|
||||
[values]
|
||||
layer_height = 0.1
|
||||
|
@ -8,7 +8,7 @@ setting_version = 5
|
||||
type = quality
|
||||
quality_type = normal
|
||||
weight = 2
|
||||
material = zyyx_pro_pla
|
||||
material = generic_pla
|
||||
|
||||
[values]
|
||||
layer_height = 0.2
|
||||
|
Loading…
x
Reference in New Issue
Block a user