diff --git a/.github/workflows/conan-package-create.yml b/.github/workflows/conan-package-create.yml index f9fbbef6f7..3e5f3dbe1c 100644 --- a/.github/workflows/conan-package-create.yml +++ b/.github/workflows/conan-package-create.yml @@ -149,5 +149,5 @@ jobs: run: conan upload "*" -r cura --all -c - name: Upload the Package(s) community - if: ${{ always() && inputs.conan_upload_community == 'true' }} + if: ${{ always() && inputs.conan_upload_community == true }} run: conan upload "*" -r cura-ce -c diff --git a/.github/workflows/conan-recipe-export.yml b/.github/workflows/conan-recipe-export.yml index cab21604fe..f38c0046c9 100644 --- a/.github/workflows/conan-recipe-export.yml +++ b/.github/workflows/conan-recipe-export.yml @@ -102,5 +102,5 @@ jobs: run: conan upload "*" -r cura --all -c - name: Upload the Package(s) community - if: ${{ always() && inputs.conan_upload_community == 'true' }} + if: ${{ always() && inputs.conan_upload_community == true }} run: conan upload "*" -r cura-ce -c diff --git a/.github/workflows/conan-recipe-version.yml b/.github/workflows/conan-recipe-version.yml index 5a24754f03..ddadfe1781 100644 --- a/.github/workflows/conan-recipe-version.yml +++ b/.github/workflows/conan-recipe-version.yml @@ -135,6 +135,9 @@ jobs: user = "_" channel = "_" else: + if latest_branch_version.prerelease and not "." in latest_branch_version.prerelease: + # The prerealese did not contain a version number, default it to 1 + latest_branch_version.prerelease += ".1" if event_name == "pull_request": actual_version = f"{latest_branch_version.major}.{latest_branch_version.minor}.{latest_branch_version.patch}-{latest_branch_version.prerelease.lower()}+{buildmetadata}pr_{issue_number}_{no_commits}" else: diff --git a/.github/workflows/requirements-conan-package.txt b/.github/workflows/requirements-conan-package.txt index 1af154c83c..fcc1379cfa 100644 --- a/.github/workflows/requirements-conan-package.txt +++ b/.github/workflows/requirements-conan-package.txt @@ -1,2 +1,2 @@ -conan!=1.51.0,!=1.51.1,!=1.51.2 -sip==6.5.1 +conan!=1.51.0,!=1.51.1,!=1.51.2,!=1.51.3 +sip diff --git a/CuraVersion.py.jinja b/CuraVersion.py.jinja index 1c30a0b5af..1c8ff0551c 100644 --- a/CuraVersion.py.jinja +++ b/CuraVersion.py.jinja @@ -11,3 +11,4 @@ CuraCloudAPIVersion = "{{ cura_cloud_api_version }}" CuraCloudAccountAPIRoot = "{{ cura_cloud_account_api_root }}" CuraMarketplaceRoot = "{{ cura_marketplace_root }}" CuraDigitalFactoryURL = "{{ cura_digital_factory_url }}" +CuraLatestURL = "{{ cura_latest_url }}" diff --git a/conandata.yml b/conandata.yml index 2933fd8437..4b9d8dd612 100644 --- a/conandata.yml +++ b/conandata.yml @@ -20,6 +20,9 @@ - "fdm_materials/(latest)@ultimaker/testing" - "cura_binary_data/(latest)@ultimaker/testing" - "cpython/3.10.4" + internal_requirements: + - "fdm_materials_private/(latest)@ultimaker/testing" + - "cura_private_data/(latest)@ultimaker/testing" runinfo: entrypoint: "cura_app.py" pyinstaller: @@ -32,6 +35,11 @@ package: "cura" src: "resources" dst: "share/cura/resources" + cura_private_data: + package: "cura_private_data" + src: "resources" + dst: "share/cura/resources" + internal: true uranium_plugins: package: "uranium" src: "plugins" @@ -60,6 +68,11 @@ package: "fdm_materials" src: "materials" dst: "share/cura/resources/materials" + fdm_materials_private: + package: "fdm_materials_private" + src: "resources/materials" + dst: "share/cura/resources/materials" + internal: true tcl: package: "tcl" src: "lib/tcl8.6" @@ -112,6 +125,9 @@ - "fdm_materials/(latest)@ultimaker/testing" - "cura_binary_data/(latest)@ultimaker/testing" - "cpython/3.10.4" + internal_requirements: + - "fdm_materials_private/(latest)@ultimaker/testing" + - "cura_private_data/(latest)@ultimaker/testing" runinfo: entrypoint: "cura_app.py" pyinstaller: @@ -124,6 +140,11 @@ package: "cura" src: "resources" dst: "share/cura/resources" + cura_private_data: + package: "cura_private_data" + src: "resources" + dst: "share/cura/resources" + internal: true uranium_plugins: package: "uranium" src: "plugins" @@ -152,6 +173,11 @@ package: "fdm_materials" src: "materials" dst: "share/cura/resources/materials" + fdm_materials_private: + package: "fdm_materials_private" + src: "resources/materials" + dst: "share/cura/resources/materials" + internal: true tcl: package: "tcl" src: "lib/tcl8.6" diff --git a/conanfile.py b/conanfile.py index ee7446483d..bcdaeda5d9 100644 --- a/conanfile.py +++ b/conanfile.py @@ -9,7 +9,7 @@ from conan.tools import files from conan.tools.env import VirtualRunEnv, Environment from conan.errors import ConanInvalidConfiguration -required_conan_version = ">=1.47.0" +required_conan_version = ">=1.48.0" class CuraConan(ConanFile): @@ -100,6 +100,10 @@ class CuraConan(ConanFile): def _digital_factory_url(self): return "https://digitalfactory-staging.ultimaker.com" if self._staging else "https://digitalfactory.ultimaker.com" + @property + def _cura_latest_url(self): + return "https://software.ultimaker.com/latest.json" + @property def requirements_txts(self): if self.options.devtools: @@ -161,12 +165,16 @@ class CuraConan(ConanFile): cura_cloud_api_version = self.options.cloud_api_version, cura_cloud_account_api_root = self._cloud_account_api_root, cura_marketplace_root = self._marketplace_root, - cura_digital_factory_url = self._digital_factory_url)) + cura_digital_factory_url = self._digital_factory_url, + cura_latest_url = self._cura_latest_url)) def _generate_pyinstaller_spec(self, location, entrypoint_location, icon_path, entitlements_file): pyinstaller_metadata = self._um_data()["pyinstaller"] datas = [(str(self._base_dir.joinpath("conan_install_info.json")), ".")] for data in pyinstaller_metadata["datas"].values(): + if not self.options.internal and data.get("internal", False): + continue + if "package" in data: # get the paths from conan package if data["package"] == self.name: if self.in_local_cache: @@ -248,6 +256,9 @@ class CuraConan(ConanFile): def requirements(self): for req in self._um_data()["requirements"]: self.requires(req) + if self.options.internal: + for req in self._um_data()["internal_requirements"]: + self.requires(req) def layout(self): self.folders.source = "." @@ -286,11 +297,17 @@ class CuraConan(ConanFile): self.copy("*.fdm_material", root_package = "fdm_materials", src = "@resdirs", dst = "resources/materials", keep_path = False) self.copy("*.sig", root_package = "fdm_materials", src = "@resdirs", dst = "resources/materials", keep_path = False) + if self.options.internal: + self.copy("*.fdm_material", root_package = "fdm_materials_private", src = "@resdirs", dst = "resources/materials", keep_path = False) + self.copy("*.sig", root_package = "fdm_materials_private", src = "@resdirs", dst = "resources/materials", keep_path = False) + self.copy("*", root_package = "cura_private_data", src = self.deps_cpp_info["cura_private_data"].resdirs[0], + dst = self._share_dir.joinpath("cura", "resources"), keep_path = True) + # Copy resources of cura_binary_data self.copy("*", root_package = "cura_binary_data", src = self.deps_cpp_info["cura_binary_data"].resdirs[0], - dst = "venv/share/cura", keep_path = True) + dst = self._share_dir.joinpath("cura", "resources"), keep_path = True) self.copy("*", root_package = "cura_binary_data", src = self.deps_cpp_info["cura_binary_data"].resdirs[1], - dst = "venv/share/uranium", keep_path = True) + dst =self._share_dir.joinpath("uranium", "resources"), keep_path = True) self.copy("*.dll", src = "@bindirs", dst = self._site_packages) self.copy("*.pyd", src = "@libdirs", dst = self._site_packages) @@ -318,6 +335,15 @@ class CuraConan(ConanFile): self.copy_deps("*.sig", root_package = "fdm_materials", src = self.deps_cpp_info["fdm_materials"].resdirs[0], dst = self._share_dir.joinpath("cura", "resources", "materials"), keep_path = False) + # Copy internal resources + if self.options.internal: + self.copy_deps("*.fdm_material", root_package = "fdm_materials_private", src = self.deps_cpp_info["fdm_materials_private"].resdirs[0], + dst = self._share_dir.joinpath("cura", "resources", "materials"), keep_path = False) + self.copy_deps("*.sig", root_package = "fdm_materials_private", src = self.deps_cpp_info["fdm_materials_private"].resdirs[0], + dst = self._share_dir.joinpath("cura", "resources", "materials"), keep_path = False) + self.copy_deps("*", root_package = "cura_private_data", src = self.deps_cpp_info["cura_private_data"].resdirs[0], + dst = self._share_dir.joinpath("cura", "resources"), keep_path = True) + # Copy resources of Uranium (keep folder structure) self.copy_deps("*", root_package = "uranium", src = self.deps_cpp_info["uranium"].resdirs[0], dst = self._share_dir.joinpath("uranium", "resources"), keep_path = True) diff --git a/cura/ApplicationMetadata.py b/cura/ApplicationMetadata.py index 60d9201d8e..4938d569e7 100644 --- a/cura/ApplicationMetadata.py +++ b/cura/ApplicationMetadata.py @@ -9,12 +9,20 @@ DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura" DEFAULT_CURA_VERSION = "dev" DEFAULT_CURA_BUILD_TYPE = "" DEFAULT_CURA_DEBUG_MODE = False +DEFAULT_CURA_LATEST_URL = "https://software.ultimaker.com/latest.json" # Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for # example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the # CuraVersion.py.in template. CuraSDKVersion = "8.1.0" +try: + from cura.CuraVersion import CuraLatestURL + if CuraLatestURL == "": + CuraLatestURL = DEFAULT_CURA_LATEST_URL +except ImportError: + CuraLatestURL = DEFAULT_CURA_LATEST_URL + try: from cura.CuraVersion import CuraAppName # type: ignore if CuraAppName == "": diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index eeaead4f71..f701c94797 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -145,6 +145,8 @@ class CuraApplication(QtApplication): DefinitionChangesContainer = Resources.UserType + 10 SettingVisibilityPreset = Resources.UserType + 11 IntentInstanceContainer = Resources.UserType + 12 + AbstractMachineStack = Resources.UserType + 13 + pyqtEnum(ResourceTypes) @@ -152,6 +154,7 @@ class CuraApplication(QtApplication): super().__init__(name = ApplicationMetadata.CuraAppName, app_display_name = ApplicationMetadata.CuraAppDisplayName, version = ApplicationMetadata.CuraVersion if not ApplicationMetadata.IsAlternateVersion else ApplicationMetadata.CuraBuildType, + latest_url = ApplicationMetadata.CuraLatestURL, api_version = ApplicationMetadata.CuraSDKVersion, build_type = ApplicationMetadata.CuraBuildType, is_debug_mode = ApplicationMetadata.CuraDebugMode, @@ -422,6 +425,7 @@ class CuraApplication(QtApplication): Resources.addStorageType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes") Resources.addStorageType(self.ResourceTypes.SettingVisibilityPreset, "setting_visibility") Resources.addStorageType(self.ResourceTypes.IntentInstanceContainer, "intent") + Resources.addStorageType(self.ResourceTypes.AbstractMachineStack, "abstract_machine_instances") self._container_registry.addResourceType(self.ResourceTypes.QualityInstanceContainer, "quality") self._container_registry.addResourceType(self.ResourceTypes.QualityChangesInstanceContainer, "quality_changes") @@ -432,6 +436,7 @@ class CuraApplication(QtApplication): self._container_registry.addResourceType(self.ResourceTypes.MachineStack, "machine") self._container_registry.addResourceType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes") self._container_registry.addResourceType(self.ResourceTypes.IntentInstanceContainer, "intent") + self._container_registry.addResourceType(self.ResourceTypes.AbstractMachineStack, "abstract_machine") Resources.addType(self.ResourceTypes.QmlFiles, "qml") Resources.addType(self.ResourceTypes.Firmware, "firmware") @@ -480,6 +485,7 @@ class CuraApplication(QtApplication): ("variant", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.VariantInstanceContainer, "application/x-uranium-instancecontainer"), ("setting_visibility", SettingVisibilityPresetsModel.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.SettingVisibilityPreset, "application/x-uranium-preferences"), ("machine", 2): (Resources.DefinitionContainers, "application/x-uranium-definitioncontainer"), + ("abstract_machine", 1): (Resources.DefinitionContainers, "application/x-uranium-definitioncontainer"), ("extruder", 2): (Resources.DefinitionContainers, "application/x-uranium-definitioncontainer") } ) diff --git a/cura/Settings/AbstractMachine.py b/cura/Settings/AbstractMachine.py new file mode 100644 index 0000000000..a89201a294 --- /dev/null +++ b/cura/Settings/AbstractMachine.py @@ -0,0 +1,35 @@ +from typing import List + +from UM.Settings.ContainerStack import ContainerStack +from cura.PrinterOutput.PrinterOutputDevice import ConnectionType +from cura.Settings.GlobalStack import GlobalStack +from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase +from UM.Settings.ContainerRegistry import ContainerRegistry + + +class AbstractMachine(GlobalStack): + """ Represents a group of machines of the same type. This allows the user to select settings before selecting a printer. """ + + def __init__(self, container_id: str) -> None: + super().__init__(container_id) + self.setMetaDataEntry("type", "abstract_machine") + + def getMachines(self) -> List[ContainerStack]: + from cura.CuraApplication import CuraApplication + + application = CuraApplication.getInstance() + registry = application.getContainerRegistry() + + printer_type = self.definition.getId() + return [machine for machine in registry.findContainerStacks(type="machine") if machine.definition.id == printer_type and ConnectionType.CloudConnection in machine.configuredConnectionTypes] + + +## private: +_abstract_machine_mime = MimeType( + name = "application/x-cura-abstract-machine", + comment = "Cura Abstract Machine", + suffixes = ["global.cfg"] +) + +MimeTypeDatabase.addMimeType(_abstract_machine_mime) +ContainerRegistry.addContainerTypeByName(AbstractMachine, "abstract_machine", _abstract_machine_mime.name) diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 6ff856efcb..1a711ef919 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -108,7 +108,7 @@ class CuraContainerRegistry(ContainerRegistry): :param container_type: :type{string} Type of the container (machine, quality, ...) :param container_name: :type{string} Name to check """ - container_class = ContainerStack if container_type == "machine" else InstanceContainer + container_class = ContainerStack if "machine" in container_type else InstanceContainer return self.findContainersMetadata(container_type = container_class, id = container_name, type = container_type, ignore_case = True) or \ self.findContainersMetadata(container_type = container_class, name = container_name, type = container_type) diff --git a/cura/Settings/CuraStackBuilder.py b/cura/Settings/CuraStackBuilder.py index ff9a795c43..7eff275457 100644 --- a/cura/Settings/CuraStackBuilder.py +++ b/cura/Settings/CuraStackBuilder.py @@ -9,6 +9,7 @@ from UM.Settings.Interfaces import DefinitionContainerInterface from UM.Settings.InstanceContainer import InstanceContainer from cura.Machines.ContainerTree import ContainerTree +from .AbstractMachine import AbstractMachine from .GlobalStack import GlobalStack from .ExtruderStack import ExtruderStack @@ -27,7 +28,7 @@ class CuraStackBuilder: :return: The new global stack or None if an error occurred. """ - from cura.CuraApplication import CuraApplication + from cura.CuraApplication import CuraApplication # inline import needed due to circular import application = CuraApplication.getInstance() registry = application.getContainerRegistry() container_tree = ContainerTree.getInstance() @@ -91,7 +92,7 @@ class CuraStackBuilder: :param extruder_position: The position of the current extruder. """ - from cura.CuraApplication import CuraApplication + from cura.CuraApplication import CuraApplication # inline import needed due to circular import application = CuraApplication.getInstance() registry = application.getContainerRegistry() @@ -199,13 +200,21 @@ class CuraStackBuilder: :return: A new Global stack instance with the specified parameters. """ - - from cura.CuraApplication import CuraApplication - application = CuraApplication.getInstance() - registry = application.getContainerRegistry() - stack = GlobalStack(new_stack_id) stack.setDefinition(definition) + cls.createUserContainer(new_stack_id, definition, stack, variant_container, material_container, quality_container) + return stack + + @classmethod + def createUserContainer(cls, new_stack_id: str, definition: DefinitionContainerInterface, + stack: GlobalStack, + variant_container: "InstanceContainer", + material_container: "InstanceContainer", + quality_container: "InstanceContainer") -> None: + from cura.CuraApplication import CuraApplication + application = CuraApplication.getInstance() + + registry = application.getContainerRegistry() # Create user container user_container = cls.createUserChangesContainer(new_stack_id + "_user", definition.getId(), new_stack_id, @@ -221,8 +230,6 @@ class CuraStackBuilder: registry.addContainer(user_container) - return stack - @classmethod def createUserChangesContainer(cls, container_name: str, definition_id: str, stack_id: str, is_global_stack: bool) -> "InstanceContainer": @@ -259,3 +266,49 @@ class CuraStackBuilder: container_stack.definitionChanges = definition_changes_container return definition_changes_container + + @classmethod + def createAbstractMachine(cls, definition_id: str) -> Optional[AbstractMachine]: + """Create a new instance of an abstract machine. + + :param definition_id: The ID of the machine definition to use. + + :return: The new Abstract Machine or None if an error occurred. + """ + abstract_machine_id = definition_id + "_abstract_machine" + + from cura.CuraApplication import CuraApplication + application = CuraApplication.getInstance() + registry = application.getContainerRegistry() + container_tree = ContainerTree.getInstance() + + if registry.findContainerStacks(type = "abstract_machine", id = abstract_machine_id): + # This abstract machine already exists + return None + + match registry.findDefinitionContainers(type = "machine", id = definition_id): + case []: + # It should not be possible for the definition to be missing since an abstract machine will only + # be created as a result of a machine with definition_id being created. + Logger.error(f"Definition {definition_id} was not found!") + return None + case [machine_definition, *_definitions]: + machine_node = container_tree.machines[machine_definition.getId()] + name = machine_definition.getName() + + stack = AbstractMachine(abstract_machine_id) + stack.setDefinition(machine_definition) + cls.createUserContainer( + name, + machine_definition, + stack, + application.empty_variant_container, + application.empty_material_container, + machine_node.preferredGlobalQuality().container, + ) + + stack.setName(name) + + registry.addContainer(stack) + + return stack diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index f7f659124c..30bbf68f6c 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -400,11 +400,13 @@ class CloudOutputDeviceManager: # We do not use use MachineManager.addMachine here because we need to set the cluster ID before activating it. new_machine = CuraStackBuilder.createMachine(device.name, device.printerType, show_warning_message=False) if not new_machine: - Logger.log("e", "Failed creating a new machine") + Logger.error(f"Failed creating a new machine for {device.name}") return False self._setOutputDeviceMetadata(device, new_machine) + _abstract_machine = CuraStackBuilder.createAbstractMachine(device.printerType) + if activate: CuraApplication.getInstance().getMachineManager().setActiveMachine(new_machine.getId()) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 0c939e03f2..ce641cf032 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -1225,20 +1225,7 @@ "default_value": 0.3, "value": "min_wall_line_width", "type": "float", - "settable_per_mesh": true, - "children": - { - "wall_split_middle_threshold": { - "label": "Split Middle Line Threshold", - "description": "The smallest line width, as a factor of the normal line width, above which the middle line (if there is one) will be split into two. Reduce this setting to use more, thinner lines. Increase to use fewer, wider lines. Note that this applies -as if- the entire shape should be filled with wall, so the middle here refers to the middle of the object between two outer edges of the shape, even if there actually is fill or (other) skin in the print instead of wall.", - "type": "float", - "unit": "%", - "default_value": 50, - "value": "max(1, min(99, 100 * (2 * min_even_wall_line_width - wall_line_width_0) / wall_line_width_0))", - "minimum_value": "1", - "maximum_value": "99" - } - } + "settable_per_mesh": true }, "min_odd_wall_line_width": { @@ -1250,20 +1237,7 @@ "default_value": 0.3, "value": "min_wall_line_width", "type": "float", - "settable_per_mesh": true, - "children": - { - "wall_add_middle_threshold": { - "label": "Add Middle Line Threshold", - "description": "The smallest line width, as a factor of the normal line width, above which a middle line (if there wasn't one already) will be added. Reduce this setting to use more, thinner lines. Increase to use fewer, wider lines. Note that this applies -as if- the entire shape should be filled with wall, so the middle here refers to the middle of the object between two outer edges of the shape, even if there actually is fill or (other) skin in the print instead of wall.", - "type": "float", - "unit": "%", - "default_value": 75, - "value": "max(1, min(99, 100 * min_odd_wall_line_width / wall_line_width_x))", - "minimum_value": "1", - "maximum_value": "99" - } - } + "settable_per_mesh": true } } }, @@ -8049,7 +8023,7 @@ "label": "Remove Raft Inside Corners", "description": "Remove inside corners from the raft, causing the raft to become convex.", "type": "bool", - "default_value": false, + "default_value": true, "resolve": "any(extruderValues('raft_remove_inside_corners'))", "enabled": "resolveOrValue('adhesion_type') == 'raft'", "settable_per_mesh": false, diff --git a/resources/qml/PrintSetupSelector/Recommended/RecommendedInfillDensitySelector.qml b/resources/qml/PrintSetupSelector/Recommended/RecommendedInfillDensitySelector.qml index bb3b0cdbec..0317cb7814 100644 --- a/resources/qml/PrintSetupSelector/Recommended/RecommendedInfillDensitySelector.qml +++ b/resources/qml/PrintSetupSelector/Recommended/RecommendedInfillDensitySelector.qml @@ -51,7 +51,17 @@ Item { target: infillSlider property: "value" - value: parseInt(infillDensity.properties.value) + value: { + // The infill slider has a max value of 100. When it is given a value > 100 onValueChanged updates the setting to be 100. + // When changing to an intent with infillDensity > 100, it would always be clamped to 100. + // This will force the slider to ignore the first onValueChanged for values > 100 so higher values can be set. + var density = parseInt(infillDensity.properties.value) + if (density > 100) { + infillSlider.ignoreValueChange = true + } + + return density + } } // Here are the elements that are shown in the left column @@ -84,6 +94,8 @@ Item { id: infillSlider + property var ignoreValueChange: false + width: parent.width height: UM.Theme.getSize("print_setup_slider_handle").height // The handle is the widest element of the slider @@ -157,7 +169,13 @@ Item target: infillSlider function onValueChanged() { - // Don't round the value if it's already the same + if (infillSlider.ignoreValueChange) + { + infillSlider.ignoreValueChange = false + return + } + + // Don't update if the setting value, if the slider has the same value if (parseInt(infillDensity.properties.value) == infillSlider.value) { return