mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-06 14:37:29 -06:00
Merge branch 'main' into SearchAndReplace
This commit is contained in:
commit
28a21cb314
377 changed files with 13889 additions and 10742 deletions
|
@ -196,7 +196,7 @@ class DigitalFactoryApiClient:
|
|||
url = "{}/projects/{}/files".format(self.CURA_API_ROOT, library_project_id)
|
||||
self._http.get(url,
|
||||
scope = self._scope,
|
||||
callback = self._parseCallback(on_finished, DigitalFactoryFileResponse, failed),
|
||||
callback = self._parseCallback(on_finished, DigitalFactoryFileResponse, failed, default_values = {'username': ''}),
|
||||
error_callback = failed,
|
||||
timeout = self.DEFAULT_REQUEST_TIMEOUT)
|
||||
|
||||
|
@ -205,7 +205,8 @@ class DigitalFactoryApiClient:
|
|||
Callable[[List[CloudApiClientModel]], Any]],
|
||||
model: Type[CloudApiClientModel],
|
||||
on_error: Optional[Callable] = None,
|
||||
pagination_manager: Optional[PaginationManager] = None) -> Callable[[QNetworkReply], None]:
|
||||
pagination_manager: Optional[PaginationManager] = None,
|
||||
default_values: Dict[str, str] = None) -> Callable[[QNetworkReply], None]:
|
||||
|
||||
"""
|
||||
Creates a callback function so that it includes the parsing of the response into the correct model.
|
||||
|
@ -234,7 +235,7 @@ class DigitalFactoryApiClient:
|
|||
if status_code >= 300 and on_error is not None:
|
||||
on_error()
|
||||
else:
|
||||
self._parseModels(response, on_finished, model, pagination_manager = pagination_manager)
|
||||
self._parseModels(response, on_finished, model, pagination_manager = pagination_manager, default_values = default_values)
|
||||
|
||||
self._anti_gc_callbacks.append(parse)
|
||||
return parse
|
||||
|
@ -262,7 +263,8 @@ class DigitalFactoryApiClient:
|
|||
on_finished: Union[Callable[[CloudApiClientModel], Any],
|
||||
Callable[[List[CloudApiClientModel]], Any]],
|
||||
model_class: Type[CloudApiClientModel],
|
||||
pagination_manager: Optional[PaginationManager] = None) -> None:
|
||||
pagination_manager: Optional[PaginationManager] = None,
|
||||
default_values: Dict[str, str] = None) -> None:
|
||||
"""Parses the given models and calls the correct callback depending on the result.
|
||||
|
||||
:param response: The response from the server, after being converted to a dict.
|
||||
|
@ -279,7 +281,10 @@ class DigitalFactoryApiClient:
|
|||
if "links" in response and pagination_manager:
|
||||
pagination_manager.setLinks(response["links"])
|
||||
if isinstance(data, list):
|
||||
results = [model_class(**c) for c in data] # type: List[CloudApiClientModel]
|
||||
results = [] # type: List[CloudApiClientModel]
|
||||
for model_data in data:
|
||||
complete_model_data = (default_values | model_data) if default_values is not None else model_data
|
||||
results.append(model_class(**complete_model_data))
|
||||
on_finished_list = cast(Callable[[List[CloudApiClientModel]], Any], on_finished)
|
||||
on_finished_list(results)
|
||||
else:
|
||||
|
|
|
@ -298,8 +298,14 @@ class FlavorParser:
|
|||
position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
|
||||
return position
|
||||
|
||||
def processMCode(self, M: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
|
||||
pass
|
||||
def processMCode(self, M: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> None:
|
||||
# Set extrusion mode
|
||||
if M == 82:
|
||||
# Set absolute extrusion mode
|
||||
self._is_absolute_extrusion = True
|
||||
elif M == 83:
|
||||
# Set relative extrusion mode
|
||||
self._is_absolute_extrusion = False
|
||||
|
||||
_type_keyword = ";TYPE:"
|
||||
_layer_keyword = ";LAYER:"
|
||||
|
|
|
@ -11,14 +11,6 @@ class RepRapFlavorParser(FlavorParser.FlavorParser):
|
|||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def processMCode(self, M, line, position, path):
|
||||
if M == 82:
|
||||
# Set absolute extrusion mode
|
||||
self._is_absolute_extrusion = True
|
||||
elif M == 83:
|
||||
# Set relative extrusion mode
|
||||
self._is_absolute_extrusion = False
|
||||
|
||||
def _gCode90(self, position, params, path):
|
||||
"""Set the absolute positioning
|
||||
|
||||
|
|
|
@ -214,7 +214,7 @@ Item
|
|||
|
||||
settingStoreIndex: propertyStoreIndex
|
||||
|
||||
labelText: catalog.i18nc("@label", "Y min")
|
||||
labelText: catalog.i18nc("@label", "Y min ( '-' towards back)")
|
||||
labelFont: base.labelFont
|
||||
labelWidth: base.labelWidth
|
||||
controlWidth: base.controlWidth
|
||||
|
@ -254,7 +254,7 @@ Item
|
|||
settingKey: "machine_head_with_fans_polygon"
|
||||
settingStoreIndex: propertyStoreIndex
|
||||
|
||||
labelText: catalog.i18nc("@label", "Y max")
|
||||
labelText: catalog.i18nc("@label", "Y max ( '+' towards front)")
|
||||
labelFont: base.labelFont
|
||||
labelWidth: base.labelWidth
|
||||
controlWidth: base.controlWidth
|
||||
|
|
|
@ -46,6 +46,13 @@ class MakerbotWriter(MeshWriter):
|
|||
suffixes=["makerbot"]
|
||||
)
|
||||
)
|
||||
MimeTypeDatabase.addMimeType(
|
||||
MimeType(
|
||||
name="application/x-makerbot-replicator_plus",
|
||||
comment="Makerbot Toolpath Package",
|
||||
suffixes=["makerbot"]
|
||||
)
|
||||
)
|
||||
|
||||
_PNG_FORMAT = [
|
||||
{"prefix": "isometric_thumbnail", "width": 120, "height": 120},
|
||||
|
@ -114,6 +121,8 @@ class MakerbotWriter(MeshWriter):
|
|||
filename, filedata = "print.gcode", gcode_text_io.getvalue()
|
||||
case "application/x-makerbot":
|
||||
filename, filedata = "print.jsontoolpath", du.gcode_2_miracle_jtp(gcode_text_io.getvalue())
|
||||
case "application/x-makerbot-replicator_plus":
|
||||
filename, filedata = "print.jsontoolpath", du.gcode_2_miracle_jtp(gcode_text_io.getvalue(), nb_extruders=1)
|
||||
case _:
|
||||
raise Exception("Unsupported Mime type")
|
||||
|
||||
|
@ -250,7 +259,7 @@ class MakerbotWriter(MeshWriter):
|
|||
meta["preferences"] = dict()
|
||||
bounds = application.getBuildVolume().getBoundingBox()
|
||||
meta["preferences"]["instance0"] = {
|
||||
"machineBounds": [bounds.right, bounds.back, bounds.left, bounds.front] if bounds is not None else None,
|
||||
"machineBounds": [bounds.right, bounds.front, bounds.left, bounds.back] if bounds is not None else None,
|
||||
"printMode": CuraApplication.getInstance().getIntentManager().currentIntentCategory,
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,12 @@ def getMetaData():
|
|||
"description": catalog.i18nc("@item:inlistbox", "Makerbot Sketch Printfile"),
|
||||
"mime_type": "application/x-makerbot-sketch",
|
||||
"mode": MakerbotWriter.MakerbotWriter.OutputMode.BinaryMode,
|
||||
},
|
||||
{
|
||||
"extension": file_extension,
|
||||
"description": catalog.i18nc("@item:inlistbox", "Makerbot Replicator+ Printfile"),
|
||||
"mime_type": "application/x-makerbot-replicator_plus",
|
||||
"mode": MakerbotWriter.MakerbotWriter.OutputMode.BinaryMode,
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -222,12 +222,11 @@ class SimulationView(CuraView):
|
|||
|
||||
self.setPath(i + fractional_value)
|
||||
|
||||
def advanceTime(self, time_increase: float) -> bool:
|
||||
def advanceTime(self, time_increase: float) -> None:
|
||||
"""
|
||||
Advance the time by the given amount.
|
||||
|
||||
:param time_increase: The amount of time to advance (in seconds).
|
||||
:return: True if the time was advanced, False if the end of the simulation was reached.
|
||||
"""
|
||||
total_duration = 0.0
|
||||
if len(self.cumulativeLineDuration()) > 0:
|
||||
|
@ -237,15 +236,13 @@ class SimulationView(CuraView):
|
|||
# If we have reached the end of the simulation, go to the next layer.
|
||||
if self.getCurrentLayer() == self.getMaxLayers():
|
||||
# If we are already at the last layer, go to the first layer.
|
||||
self.setTime(total_duration)
|
||||
return False
|
||||
|
||||
# advance to the next layer, and reset the time
|
||||
self.setLayer(self.getCurrentLayer() + 1)
|
||||
self.setLayer(0)
|
||||
else:
|
||||
# advance to the next layer, and reset the time
|
||||
self.setLayer(self.getCurrentLayer() + 1)
|
||||
self.setTime(0.0)
|
||||
else:
|
||||
self.setTime(self._current_time + time_increase)
|
||||
return True
|
||||
|
||||
def cumulativeLineDuration(self) -> List[float]:
|
||||
# Make sure _cumulative_line_duration is initialized properly
|
||||
|
|
|
@ -144,9 +144,7 @@ Item
|
|||
{
|
||||
// divide by 1000 to account for ms to s conversion
|
||||
const advance_time = simulationTimer.interval / 1000.0;
|
||||
if (!UM.SimulationView.advanceTime(advance_time)) {
|
||||
playButton.pauseSimulation();
|
||||
}
|
||||
UM.SimulationView.advanceTime(advance_time);
|
||||
// The status must be set here instead of in the resumeSimulation function otherwise it won't work
|
||||
// correctly, because part of the logic is in this trigger function.
|
||||
isSimulationPlaying = true;
|
||||
|
|
|
@ -54,9 +54,9 @@ class SimulationViewProxy(QObject):
|
|||
def currentPath(self):
|
||||
return self._simulation_view.getCurrentPath()
|
||||
|
||||
@pyqtSlot(float, result=bool)
|
||||
def advanceTime(self, duration: float) -> bool:
|
||||
return self._simulation_view.advanceTime(duration)
|
||||
@pyqtSlot(float)
|
||||
def advanceTime(self, duration: float) -> None:
|
||||
self._simulation_view.advanceTime(duration)
|
||||
|
||||
@pyqtProperty(int, notify=currentPathChanged)
|
||||
def minimumPath(self):
|
||||
|
|
|
@ -360,8 +360,8 @@ geometry41core =
|
|||
((v_prev_line_type[0] != 1) && (v_line_type[0] == 1)) ||
|
||||
((v_prev_line_type[0] != 4) && (v_line_type[0] == 4))
|
||||
)) {
|
||||
float w = size_x;
|
||||
float h = size_y;
|
||||
float w = max(0.05, size_x);
|
||||
float h = max(0.05, size_y);
|
||||
|
||||
myEmitVertex(v_vertex[0] + vec3( w, h, w), u_starts_color, normalize(vec3( 1.0, 1.0, 1.0)), viewProjectionMatrix * (gl_in[0].gl_Position + vec4( w, h, w, 0.0))); // Front-top-left
|
||||
myEmitVertex(v_vertex[0] + vec3(-w, h, w), u_starts_color, normalize(vec3(-1.0, 1.0, 1.0)), viewProjectionMatrix * (gl_in[0].gl_Position + vec4(-w, h, w, 0.0))); // Front-top-right
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
# Copyright (c) 2023 UltiMaker
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import datetime
|
||||
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import time
|
||||
from typing import Optional, Set, TYPE_CHECKING
|
||||
from typing import Any, Optional, Set, TYPE_CHECKING
|
||||
|
||||
from PyQt6.QtCore import pyqtSlot, QObject
|
||||
from PyQt6.QtNetwork import QNetworkRequest
|
||||
|
@ -33,7 +34,18 @@ class SliceInfo(QObject, Extension):
|
|||
no model files are being sent (Just a SHA256 hash of the model).
|
||||
"""
|
||||
|
||||
info_url = "https://stats.ultimaker.com/api/cura"
|
||||
info_url = "https://statistics.ultimaker.com/api/v2/cura/slice"
|
||||
|
||||
_adjust_flattened_names = {
|
||||
"extruders_extruder": "extruders",
|
||||
"extruders_settings": "extruders",
|
||||
"models_model": "models",
|
||||
"models_transformation_data": "models_transformation",
|
||||
"print_settings_": "",
|
||||
"print_times": "print_time",
|
||||
"active_machine_": "",
|
||||
"slice_uuid": "slice_id",
|
||||
}
|
||||
|
||||
def __init__(self, parent = None):
|
||||
QObject.__init__(self, parent)
|
||||
|
@ -112,6 +124,26 @@ class SliceInfo(QObject, Extension):
|
|||
|
||||
return list(sorted(user_modified_setting_keys))
|
||||
|
||||
def _flattenData(self, data: Any, result: dict, current_flat_key: Optional[str] = None, lift_list: bool = False) -> None:
|
||||
if isinstance(data, dict):
|
||||
for key, value in data.items():
|
||||
total_flat_key = key if current_flat_key is None else f"{current_flat_key}_{key}"
|
||||
self._flattenData(value, result, total_flat_key, lift_list)
|
||||
elif isinstance(data, list):
|
||||
for item in data:
|
||||
self._flattenData(item, result, current_flat_key, True)
|
||||
else:
|
||||
actual_flat_key = current_flat_key.lower()
|
||||
for key, value in self._adjust_flattened_names.items():
|
||||
if actual_flat_key.startswith(key):
|
||||
actual_flat_key = actual_flat_key.replace(key, value)
|
||||
if lift_list:
|
||||
if actual_flat_key not in result:
|
||||
result[actual_flat_key] = []
|
||||
result[actual_flat_key].append(data)
|
||||
else:
|
||||
result[actual_flat_key] = data
|
||||
|
||||
def _onWriteStarted(self, output_device):
|
||||
try:
|
||||
if not self._application.getPreferences().getValue("info/send_slice_info"):
|
||||
|
@ -125,8 +157,7 @@ class SliceInfo(QObject, Extension):
|
|||
global_stack = machine_manager.activeMachine
|
||||
|
||||
data = dict() # The data that we're going to submit.
|
||||
data["time_stamp"] = time.time()
|
||||
data["schema_version"] = 0
|
||||
data["schema_version"] = 1000
|
||||
data["cura_version"] = self._application.getVersion()
|
||||
data["cura_build_type"] = ApplicationMetadata.CuraBuildType
|
||||
org_id = user_profile.get("organization_id", None) if user_profile else None
|
||||
|
@ -298,6 +329,11 @@ class SliceInfo(QObject, Extension):
|
|||
"time_backend": int(round(time_backend)),
|
||||
}
|
||||
|
||||
# Massage data into format used in the DB:
|
||||
flat_data = dict()
|
||||
self._flattenData(data, flat_data)
|
||||
data = flat_data
|
||||
|
||||
# Convert data to bytes
|
||||
binary_data = json.dumps(data).encode("utf-8")
|
||||
|
||||
|
|
|
@ -331,7 +331,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
return False
|
||||
|
||||
[printer, *_] = self._printers
|
||||
return printer.type in ("MakerBot Method X", "MakerBot Method XL", "MakerBot Sketch")
|
||||
return printer.type in ("MakerBot Method", "MakerBot Method X", "MakerBot Method XL", "MakerBot Sketch", "MakerBot Sketch Large", "MakerBot Sketch Sprint")
|
||||
|
||||
@pyqtProperty(bool, notify=_cloudClusterPrintersChanged)
|
||||
def supportsPrintJobActions(self) -> bool:
|
||||
|
|
|
@ -3,5 +3,7 @@
|
|||
"ultimaker_methodx": "MakerBot Method X",
|
||||
"ultimaker_methodxl": "MakerBot Method XL",
|
||||
"ultimaker_factor4": "Ultimaker Factor 4",
|
||||
"ultimaker_sketch": "MakerBot Sketch"
|
||||
"ultimaker_sketch": "MakerBot Sketch",
|
||||
"ultimaker_sketch_large": "MakerBot Sketch Large",
|
||||
"ultimaker_sketch_sprint": "MakerBot Sketch Sprint"
|
||||
}
|
||||
|
|
|
@ -97,6 +97,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||
|
||||
CuraApplication.getInstance().getOnExitCallbackManager().addCallback(self._checkActivePrintingUponAppExit)
|
||||
|
||||
CuraApplication.getInstance().getPreferences().addPreference("usb_printing/enabled", False)
|
||||
|
||||
# This is a callback function that checks if there is any printing in progress via USB when the application tries
|
||||
# to exit. If so, it will show a confirmation before
|
||||
def _checkActivePrintingUponAppExit(self) -> None:
|
||||
|
@ -144,6 +146,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||
|
||||
CuraApplication.getInstance().getController().setActiveStage("MonitorStage")
|
||||
|
||||
CuraApplication.getInstance().getPreferences().setValue("usb_printing/enabled", True)
|
||||
|
||||
#Find the g-code to print.
|
||||
gcode_textio = StringIO()
|
||||
gcode_writer = cast(MeshWriter, PluginRegistry.getInstance().getPluginObject("GCodeWriter"))
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import configparser
|
||||
import io
|
||||
from typing import Dict, Tuple, List
|
||||
|
||||
from UM.VersionUpgrade import VersionUpgrade
|
||||
|
||||
_RENAMED_SETTINGS = {
|
||||
"wall_overhang_speed_factor": "wall_overhang_speed_factors"
|
||||
} # type: Dict[str, str]
|
||||
|
||||
_NEW_SETTING_VERSION = "25"
|
||||
|
||||
|
||||
class VersionUpgrade59to510(VersionUpgrade):
|
||||
def upgradePreferences(self, serialized: str, filename: str):
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
parser.read_string(serialized)
|
||||
|
||||
# Fix 'renamed'(ish) settings for visibility
|
||||
if "visible_settings" in parser["general"]:
|
||||
all_setting_keys = parser["general"]["visible_settings"].strip().split(";")
|
||||
if all_setting_keys:
|
||||
for idx, key in enumerate(all_setting_keys):
|
||||
if key in _RENAMED_SETTINGS:
|
||||
all_setting_keys[idx] = _RENAMED_SETTINGS[key]
|
||||
parser["general"]["visible_settings"] = ";".join(all_setting_keys)
|
||||
|
||||
# Update version number.
|
||||
parser["metadata"]["setting_version"] = _NEW_SETTING_VERSION
|
||||
|
||||
result = io.StringIO()
|
||||
parser.write(result)
|
||||
return [filename], [result.getvalue()]
|
||||
|
||||
def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
|
||||
parser = configparser.ConfigParser(interpolation = None, comment_prefixes = ())
|
||||
parser.read_string(serialized)
|
||||
|
||||
# Update version number.
|
||||
parser["metadata"]["setting_version"] = _NEW_SETTING_VERSION
|
||||
|
||||
if "values" in parser:
|
||||
for old_name, new_name in _RENAMED_SETTINGS.items():
|
||||
if old_name in parser["values"]:
|
||||
parser["values"][new_name] = parser["values"][old_name]
|
||||
del parser["values"][old_name]
|
||||
if "wall_overhang_speed_factors" in parser["values"]:
|
||||
old_value = float(parser["values"]["wall_overhang_speed_factors"])
|
||||
new_value = [max(1, int(round(old_value)))]
|
||||
parser["values"]["wall_overhang_speed_factor"] = str(new_value)
|
||||
|
||||
result = io.StringIO()
|
||||
parser.write(result)
|
||||
return [filename], [result.getvalue()]
|
||||
|
||||
def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
parser.read_string(serialized)
|
||||
|
||||
# Update version number.
|
||||
if "metadata" not in parser:
|
||||
parser["metadata"] = {}
|
||||
|
||||
parser["metadata"]["setting_version"] = _NEW_SETTING_VERSION
|
||||
|
||||
result = io.StringIO()
|
||||
parser.write(result)
|
||||
return [filename], [result.getvalue()]
|
60
plugins/VersionUpgrade/VersionUpgrade59to510/__init__.py
Normal file
60
plugins/VersionUpgrade/VersionUpgrade59to510/__init__.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
# Copyright (c) 2024 UltiMaker
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Any, Dict, TYPE_CHECKING
|
||||
|
||||
from . import VersionUpgrade59to510
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Application import Application
|
||||
|
||||
upgrade = VersionUpgrade59to510.VersionUpgrade59to510()
|
||||
|
||||
def getMetaData() -> Dict[str, Any]:
|
||||
return {
|
||||
"version_upgrade": {
|
||||
# From To Upgrade function
|
||||
("preferences", 7000024): ("preferences", 7000025, upgrade.upgradePreferences),
|
||||
("machine_stack", 6000024): ("machine_stack", 6000025, upgrade.upgradeStack),
|
||||
("extruder_train", 6000024): ("extruder_train", 6000025, upgrade.upgradeStack),
|
||||
("definition_changes", 4000024): ("definition_changes", 4000025, upgrade.upgradeInstanceContainer),
|
||||
("quality_changes", 4000024): ("quality_changes", 4000025, upgrade.upgradeInstanceContainer),
|
||||
("quality", 4000024): ("quality", 4000025, upgrade.upgradeInstanceContainer),
|
||||
("user", 4000024): ("user", 4000025, upgrade.upgradeInstanceContainer),
|
||||
("intent", 4000024): ("intent", 4000025, upgrade.upgradeInstanceContainer),
|
||||
},
|
||||
"sources": {
|
||||
"preferences": {
|
||||
"get_version": upgrade.getCfgVersion,
|
||||
"location": {"."}
|
||||
},
|
||||
"machine_stack": {
|
||||
"get_version": upgrade.getCfgVersion,
|
||||
"location": {"./machine_instances"}
|
||||
},
|
||||
"extruder_train": {
|
||||
"get_version": upgrade.getCfgVersion,
|
||||
"location": {"./extruders"}
|
||||
},
|
||||
"definition_changes": {
|
||||
"get_version": upgrade.getCfgVersion,
|
||||
"location": {"./definition_changes"}
|
||||
},
|
||||
"quality_changes": {
|
||||
"get_version": upgrade.getCfgVersion,
|
||||
"location": {"./quality_changes"}
|
||||
},
|
||||
"quality": {
|
||||
"get_version": upgrade.getCfgVersion,
|
||||
"location": {"./quality"}
|
||||
},
|
||||
"user": {
|
||||
"get_version": upgrade.getCfgVersion,
|
||||
"location": {"./user"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def register(app: "Application") -> Dict[str, Any]:
|
||||
return {"version_upgrade": upgrade}
|
8
plugins/VersionUpgrade/VersionUpgrade59to510/plugin.json
Normal file
8
plugins/VersionUpgrade/VersionUpgrade59to510/plugin.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "Version Upgrade 5.9 to 5.10",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Upgrades configurations from Cura 5.9 to Cura 5.10",
|
||||
"api": 8,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue