mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-12 01:07:52 -06:00
Merge branch 'master' into WIP_onboarding
This commit is contained in:
commit
ae9395aebb
13 changed files with 69 additions and 104 deletions
|
@ -42,7 +42,14 @@ class CuraFormulaFunctions:
|
||||||
try:
|
try:
|
||||||
extruder_stack = global_stack.extruders[str(extruder_position)]
|
extruder_stack = global_stack.extruders[str(extruder_position)]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
Logger.log("w", "Value for %s of extruder %s was requested, but that extruder is not available" % (property_key, extruder_position))
|
if extruder_position != 0:
|
||||||
|
Logger.log("w", "Value for %s of extruder %s was requested, but that extruder is not available. Returning the result form extruder 0 instead" % (property_key, extruder_position))
|
||||||
|
# This fixes a very specific fringe case; If a profile was created for a custom printer and one of the
|
||||||
|
# extruder settings has been set to non zero and the profile is loaded for a machine that has only a single extruder
|
||||||
|
# it would cause all kinds of issues (and eventually a crash).
|
||||||
|
# See https://github.com/Ultimaker/Cura/issues/5535
|
||||||
|
return self.getValueInExtruder(0, property_key, context)
|
||||||
|
Logger.log("w", "Value for %s of extruder %s was requested, but that extruder is not available. " % (property_key, extruder_position))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
value = extruder_stack.getRawProperty(property_key, "value", context = context)
|
value = extruder_stack.getRawProperty(property_key, "value", context = context)
|
||||||
|
|
|
@ -85,7 +85,15 @@ class GlobalStack(CuraContainerStack):
|
||||||
# Requesting it from the metadata actually gets them as strings (as that's what you get from serializing).
|
# Requesting it from the metadata actually gets them as strings (as that's what you get from serializing).
|
||||||
# But we do want them returned as a list of ints (so the rest of the code can directly compare)
|
# But we do want them returned as a list of ints (so the rest of the code can directly compare)
|
||||||
connection_types = self.getMetaDataEntry("connection_type", "").split(",")
|
connection_types = self.getMetaDataEntry("connection_type", "").split(",")
|
||||||
return [int(connection_type) for connection_type in connection_types if connection_type != ""]
|
result = []
|
||||||
|
for connection_type in connection_types:
|
||||||
|
if connection_type != "":
|
||||||
|
try:
|
||||||
|
result.append(int(connection_type))
|
||||||
|
except ValueError:
|
||||||
|
# We got invalid data, probably a None.
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
## \sa configuredConnectionTypes
|
## \sa configuredConnectionTypes
|
||||||
def addConfiguredConnectionType(self, connection_type: int) -> None:
|
def addConfiguredConnectionType(self, connection_type: int) -> None:
|
||||||
|
|
|
@ -14,7 +14,7 @@ Rectangle
|
||||||
Column
|
Column
|
||||||
{
|
{
|
||||||
height: childrenRect.height + 2 * padding
|
height: childrenRect.height + 2 * padding
|
||||||
spacing: UM.Theme.getSize("toolbox_showcase_spacing").width
|
spacing: UM.Theme.getSize("default_margin").width
|
||||||
width: parent.width
|
width: parent.width
|
||||||
padding: UM.Theme.getSize("wide_margin").height
|
padding: UM.Theme.getSize("wide_margin").height
|
||||||
Label
|
Label
|
||||||
|
|
|
@ -61,7 +61,7 @@ Button
|
||||||
{
|
{
|
||||||
target: label
|
target: label
|
||||||
font: UM.Theme.getFont("medium_bold")
|
font: UM.Theme.getFont("medium_bold")
|
||||||
color: UM.Theme.getColor("toolbox_header_button_text_active")
|
color: UM.Theme.getColor("action_button_text")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -145,9 +145,17 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
|
||||||
|
|
||||||
## Checks whether the given network key is found in the cloud's host name
|
## Checks whether the given network key is found in the cloud's host name
|
||||||
def matchesNetworkKey(self, network_key: str) -> bool:
|
def matchesNetworkKey(self, network_key: str) -> bool:
|
||||||
# A network key looks like "ultimakersystem-aabbccdd0011._ultimaker._tcp.local."
|
# Typically, a network key looks like "ultimakersystem-aabbccdd0011._ultimaker._tcp.local."
|
||||||
# the host name should then be "ultimakersystem-aabbccdd0011"
|
# the host name should then be "ultimakersystem-aabbccdd0011"
|
||||||
return network_key.startswith(self.clusterData.host_name)
|
if network_key.startswith(self.clusterData.host_name):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# However, for manually added printers, the local IP address is used in lieu of a proper
|
||||||
|
# network key, so check for that as well
|
||||||
|
if self.clusterData.host_internal_ip is not None and network_key.find(self.clusterData.host_internal_ip):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
## Set all the interface elements and texts for this output device.
|
## Set all the interface elements and texts for this output device.
|
||||||
def _setInterfaceElements(self) -> None:
|
def _setInterfaceElements(self) -> None:
|
||||||
|
|
|
@ -16,13 +16,14 @@ class CloudClusterResponse(BaseCloudModel):
|
||||||
# \param status: The status of the cluster authentication (active or inactive).
|
# \param status: The status of the cluster authentication (active or inactive).
|
||||||
# \param host_version: The firmware version of the cluster host. This is where the Stardust client is running on.
|
# \param host_version: The firmware version of the cluster host. This is where the Stardust client is running on.
|
||||||
def __init__(self, cluster_id: str, host_guid: str, host_name: str, is_online: bool, status: str,
|
def __init__(self, cluster_id: str, host_guid: str, host_name: str, is_online: bool, status: str,
|
||||||
host_version: Optional[str] = None, **kwargs) -> None:
|
host_internal_ip: Optional[str] = None, host_version: Optional[str] = None, **kwargs) -> None:
|
||||||
self.cluster_id = cluster_id
|
self.cluster_id = cluster_id
|
||||||
self.host_guid = host_guid
|
self.host_guid = host_guid
|
||||||
self.host_name = host_name
|
self.host_name = host_name
|
||||||
self.status = status
|
self.status = status
|
||||||
self.is_online = is_online
|
self.is_online = is_online
|
||||||
self.host_version = host_version
|
self.host_version = host_version
|
||||||
|
self.host_internal_ip = host_internal_ip
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
# Validates the model, raising an exception if the model is invalid.
|
# Validates the model, raising an exception if the model is invalid.
|
||||||
|
|
|
@ -77,13 +77,15 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
|
||||||
|
|
||||||
self.setIconName("print")
|
self.setIconName("print")
|
||||||
|
|
||||||
if PluginRegistry.getInstance() is not None:
|
self._output_controller = LegacyUM3PrinterOutputController(self)
|
||||||
|
|
||||||
|
def _createMonitorViewFromQML(self) -> None:
|
||||||
|
if self._monitor_view_qml_path is None and PluginRegistry.getInstance() is not None:
|
||||||
self._monitor_view_qml_path = os.path.join(
|
self._monitor_view_qml_path = os.path.join(
|
||||||
PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
|
PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
|
||||||
"resources", "qml", "MonitorStage.qml"
|
"resources", "qml", "MonitorStage.qml"
|
||||||
)
|
)
|
||||||
|
super()._createMonitorViewFromQML()
|
||||||
self._output_controller = LegacyUM3PrinterOutputController(self)
|
|
||||||
|
|
||||||
def _onAuthenticationStateChanged(self):
|
def _onAuthenticationStateChanged(self):
|
||||||
# We only accept commands if we are authenticated.
|
# We only accept commands if we are authenticated.
|
||||||
|
|
|
@ -7,6 +7,7 @@ from UM.OutputDevice.OutputDeviceManager import OutputDeviceManager
|
||||||
from cura.UltimakerCloudAuthentication import CuraCloudAPIRoot
|
from cura.UltimakerCloudAuthentication import CuraCloudAPIRoot
|
||||||
from ...src.Cloud import CloudApiClient
|
from ...src.Cloud import CloudApiClient
|
||||||
from ...src.Cloud import CloudOutputDeviceManager
|
from ...src.Cloud import CloudOutputDeviceManager
|
||||||
|
from ...src.Cloud.Models.CloudClusterResponse import CloudClusterResponse
|
||||||
from .Fixtures import parseFixture, readFixture
|
from .Fixtures import parseFixture, readFixture
|
||||||
from .NetworkManagerMock import NetworkManagerMock, FakeSignal
|
from .NetworkManagerMock import NetworkManagerMock, FakeSignal
|
||||||
|
|
||||||
|
@ -55,7 +56,9 @@ class TestCloudOutputDeviceManager(TestCase):
|
||||||
devices = self.device_manager.getOutputDevices()
|
devices = self.device_manager.getOutputDevices()
|
||||||
# TODO: Check active device
|
# TODO: Check active device
|
||||||
|
|
||||||
response_clusters = self.clusters_response.get("data", [])
|
response_clusters = []
|
||||||
|
for cluster in self.clusters_response.get("data", []):
|
||||||
|
response_clusters.append(CloudClusterResponse(**cluster).toDict())
|
||||||
manager_clusters = sorted([device.clusterData.toDict() for device in self.manager._remote_clusters.values()],
|
manager_clusters = sorted([device.clusterData.toDict() for device in self.manager._remote_clusters.values()],
|
||||||
key=lambda cluster: cluster['cluster_id'], reverse=True)
|
key=lambda cluster: cluster['cluster_id'], reverse=True)
|
||||||
self.assertEqual(response_clusters, manager_clusters)
|
self.assertEqual(response_clusters, manager_clusters)
|
||||||
|
@ -97,7 +100,7 @@ class TestCloudOutputDeviceManager(TestCase):
|
||||||
|
|
||||||
self.assertTrue(self.device_manager.getOutputDevice(cluster1["cluster_id"]).isConnected())
|
self.assertTrue(self.device_manager.getOutputDevice(cluster1["cluster_id"]).isConnected())
|
||||||
self.assertIsNone(self.device_manager.getOutputDevice(cluster2["cluster_id"]))
|
self.assertIsNone(self.device_manager.getOutputDevice(cluster2["cluster_id"]))
|
||||||
self.assertEquals([], active_machine_mock.setMetaDataEntry.mock_calls)
|
self.assertEqual([], active_machine_mock.setMetaDataEntry.mock_calls)
|
||||||
|
|
||||||
def test_device_connects_by_network_key(self):
|
def test_device_connects_by_network_key(self):
|
||||||
active_machine_mock = self.app.getGlobalContainerStack.return_value
|
active_machine_mock = self.app.getGlobalContainerStack.return_value
|
||||||
|
|
|
@ -208,7 +208,7 @@ class TestSendMaterialJob(TestCase):
|
||||||
|
|
||||||
self.assertEqual(1, device_mock.createFormPart.call_count)
|
self.assertEqual(1, device_mock.createFormPart.call_count)
|
||||||
self.assertEqual(1, device_mock.postFormWithParts.call_count)
|
self.assertEqual(1, device_mock.postFormWithParts.call_count)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
[call.createFormPart("name=\"file\"; filename=\"generic_pla_white.xml.fdm_material\"", "<xml></xml>"),
|
[call.createFormPart("name=\"file\"; filename=\"generic_pla_white.xml.fdm_material\"", "<xml></xml>"),
|
||||||
call.postFormWithParts(target = "materials/", parts = ["_xXx_"], on_finished = job.sendingFinished)],
|
call.postFormWithParts(target = "materials/", parts = ["_xXx_"], on_finished = job.sendingFinished)],
|
||||||
device_mock.method_calls)
|
device_mock.method_calls)
|
||||||
|
@ -238,7 +238,7 @@ class TestSendMaterialJob(TestCase):
|
||||||
|
|
||||||
self.assertEqual(1, device_mock.createFormPart.call_count)
|
self.assertEqual(1, device_mock.createFormPart.call_count)
|
||||||
self.assertEqual(1, device_mock.postFormWithParts.call_count)
|
self.assertEqual(1, device_mock.postFormWithParts.call_count)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
[call.createFormPart("name=\"file\"; filename=\"generic_pla_white.xml.fdm_material\"", "<xml></xml>"),
|
[call.createFormPart("name=\"file\"; filename=\"generic_pla_white.xml.fdm_material\"", "<xml></xml>"),
|
||||||
call.postFormWithParts(target = "materials/", parts = ["_xXx_"], on_finished = job.sendingFinished)],
|
call.postFormWithParts(target = "materials/", parts = ["_xXx_"], on_finished = job.sendingFinished)],
|
||||||
device_mock.method_calls)
|
device_mock.method_calls)
|
||||||
|
|
|
@ -4092,7 +4092,7 @@
|
||||||
"description": "Amount of offset applied to all support polygons in each layer. Positive values can smooth out the support areas and result in more sturdy support.",
|
"description": "Amount of offset applied to all support polygons in each layer. Positive values can smooth out the support areas and result in more sturdy support.",
|
||||||
"unit": "mm",
|
"unit": "mm",
|
||||||
"type": "float",
|
"type": "float",
|
||||||
"default_value": 0.2,
|
"default_value": 0,
|
||||||
"limit_to_extruder": "support_infill_extruder_nr",
|
"limit_to_extruder": "support_infill_extruder_nr",
|
||||||
"minimum_value_warning": "-1 * machine_nozzle_size",
|
"minimum_value_warning": "-1 * machine_nozzle_size",
|
||||||
"maximum_value_warning": "10 * machine_nozzle_size",
|
"maximum_value_warning": "10 * machine_nozzle_size",
|
||||||
|
|
|
@ -68,6 +68,7 @@ Column
|
||||||
|
|
||||||
property var printMaterialLengths: PrintInformation.materialLengths
|
property var printMaterialLengths: PrintInformation.materialLengths
|
||||||
property var printMaterialWeights: PrintInformation.materialWeights
|
property var printMaterialWeights: PrintInformation.materialWeights
|
||||||
|
property var printMaterialCosts: PrintInformation.materialCosts
|
||||||
|
|
||||||
text:
|
text:
|
||||||
{
|
{
|
||||||
|
@ -77,6 +78,7 @@ Column
|
||||||
}
|
}
|
||||||
var totalLengths = 0
|
var totalLengths = 0
|
||||||
var totalWeights = 0
|
var totalWeights = 0
|
||||||
|
var totalCosts = 0.0
|
||||||
if (printMaterialLengths)
|
if (printMaterialLengths)
|
||||||
{
|
{
|
||||||
for(var index = 0; index < printMaterialLengths.length; index++)
|
for(var index = 0; index < printMaterialLengths.length; index++)
|
||||||
|
@ -85,9 +87,16 @@ Column
|
||||||
{
|
{
|
||||||
totalLengths += printMaterialLengths[index]
|
totalLengths += printMaterialLengths[index]
|
||||||
totalWeights += Math.round(printMaterialWeights[index])
|
totalWeights += Math.round(printMaterialWeights[index])
|
||||||
|
var cost = printMaterialCosts[index] == undefined ? 0.0 : printMaterialCosts[index]
|
||||||
|
totalCosts += cost
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(totalCosts > 0)
|
||||||
|
{
|
||||||
|
var costString = "%1 %2".arg(UM.Preferences.getValue("cura/currency")).arg(totalCosts.toFixed(2))
|
||||||
|
return totalWeights + "g · " + totalLengths.toFixed(2) + "m · " + costString
|
||||||
|
}
|
||||||
return totalWeights + "g · " + totalLengths.toFixed(2) + "m"
|
return totalWeights + "g · " + totalLengths.toFixed(2) + "m"
|
||||||
}
|
}
|
||||||
source: UM.Theme.getIcon("spool")
|
source: UM.Theme.getIcon("spool")
|
||||||
|
|
|
@ -103,15 +103,11 @@ QtObject
|
||||||
// This property will be back-propagated when the width of the label is calculated
|
// This property will be back-propagated when the width of the label is calculated
|
||||||
property var buttonWidth: 0
|
property var buttonWidth: 0
|
||||||
|
|
||||||
background: Item
|
background: Rectangle
|
||||||
{
|
{
|
||||||
|
id: backgroundRectangle
|
||||||
implicitHeight: control.height
|
implicitHeight: control.height
|
||||||
implicitWidth: buttonWidth
|
implicitWidth: buttonWidth
|
||||||
Rectangle
|
|
||||||
{
|
|
||||||
id: buttonFace
|
|
||||||
implicitHeight: parent.height
|
|
||||||
implicitWidth: parent.width
|
|
||||||
radius: UM.Theme.getSize("action_button_radius").width
|
radius: UM.Theme.getSize("action_button_radius").width
|
||||||
|
|
||||||
color:
|
color:
|
||||||
|
@ -129,7 +125,7 @@ QtObject
|
||||||
return UM.Theme.getColor("main_window_header_button_background_inactive")
|
return UM.Theme.getColor("main_window_header_button_background_inactive")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
label: Item
|
label: Item
|
||||||
|
@ -168,6 +164,8 @@ QtObject
|
||||||
buttonWidth = width
|
buttonWidth = width
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -398,73 +396,6 @@ QtObject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combobox with items with colored rectangles
|
|
||||||
property Component combobox_color: Component
|
|
||||||
{
|
|
||||||
|
|
||||||
ComboBoxStyle
|
|
||||||
{
|
|
||||||
|
|
||||||
background: Rectangle
|
|
||||||
{
|
|
||||||
color: !enabled ? UM.Theme.getColor("setting_control_disabled") : control._hovered ? UM.Theme.getColor("setting_control_highlight") : UM.Theme.getColor("setting_control")
|
|
||||||
border.width: UM.Theme.getSize("default_lining").width
|
|
||||||
border.color: !enabled ? UM.Theme.getColor("setting_control_disabled_border") : control._hovered ? UM.Theme.getColor("setting_control_border_highlight") : UM.Theme.getColor("setting_control_border")
|
|
||||||
radius: UM.Theme.getSize("setting_control_radius").width
|
|
||||||
}
|
|
||||||
|
|
||||||
label: Item
|
|
||||||
{
|
|
||||||
Label
|
|
||||||
{
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: UM.Theme.getSize("default_lining").width
|
|
||||||
anchors.right: swatch.left
|
|
||||||
anchors.rightMargin: UM.Theme.getSize("default_lining").width
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
text: control.currentText
|
|
||||||
font: UM.Theme.getFont("default")
|
|
||||||
color: !enabled ? UM.Theme.getColor("setting_control_disabled_text") : UM.Theme.getColor("setting_control_text")
|
|
||||||
|
|
||||||
elide: Text.ElideRight
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
UM.RecolorImage
|
|
||||||
{
|
|
||||||
id: swatch
|
|
||||||
height: Math.round(control.height / 2)
|
|
||||||
width: height
|
|
||||||
anchors.right: downArrow.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.rightMargin: UM.Theme.getSize("default_margin").width
|
|
||||||
|
|
||||||
sourceSize.width: width
|
|
||||||
sourceSize.height: height
|
|
||||||
source: UM.Theme.getIcon("extruder_button")
|
|
||||||
color: (control.color_override !== "") ? control.color_override : control.color
|
|
||||||
}
|
|
||||||
|
|
||||||
UM.RecolorImage
|
|
||||||
{
|
|
||||||
id: downArrow
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: UM.Theme.getSize("default_lining").width * 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
source: UM.Theme.getIcon("arrow_bottom")
|
|
||||||
width: UM.Theme.getSize("standard_arrow").width
|
|
||||||
height: UM.Theme.getSize("standard_arrow").height
|
|
||||||
sourceSize.width: width + 5 * screenScaleFactor
|
|
||||||
sourceSize.height: width + 5 * screenScaleFactor
|
|
||||||
|
|
||||||
color: UM.Theme.getColor("setting_control_button")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property Component checkbox: Component
|
property Component checkbox: Component
|
||||||
{
|
{
|
||||||
CheckBoxStyle
|
CheckBoxStyle
|
||||||
|
|
|
@ -393,7 +393,6 @@
|
||||||
"printer_config_matched": [50, 130, 255, 255],
|
"printer_config_matched": [50, 130, 255, 255],
|
||||||
"printer_config_mismatch": [127, 127, 127, 255],
|
"printer_config_mismatch": [127, 127, 127, 255],
|
||||||
|
|
||||||
"toolbox_header_button_text_active": [0, 0, 0, 255],
|
|
||||||
"toolbox_header_button_text_inactive": [0, 0, 0, 255],
|
"toolbox_header_button_text_inactive": [0, 0, 0, 255],
|
||||||
|
|
||||||
"favorites_header_bar": [245, 245, 245, 255],
|
"favorites_header_bar": [245, 245, 245, 255],
|
||||||
|
@ -592,10 +591,8 @@
|
||||||
"toolbox_thumbnail_large": [12.0, 10.0],
|
"toolbox_thumbnail_large": [12.0, 10.0],
|
||||||
"toolbox_footer": [1.0, 4.5],
|
"toolbox_footer": [1.0, 4.5],
|
||||||
"toolbox_footer_button": [8.0, 2.5],
|
"toolbox_footer_button": [8.0, 2.5],
|
||||||
"toolbox_showcase_spacing": [1.0, 1.0],
|
|
||||||
"toolbox_header_tab": [8.0, 4.0],
|
"toolbox_header_tab": [8.0, 4.0],
|
||||||
"toolbox_detail_header": [1.0, 14.0],
|
"toolbox_detail_header": [1.0, 14.0],
|
||||||
"toolbox_detail_tile": [1.0, 8.0],
|
|
||||||
"toolbox_back_column": [6.0, 1.0],
|
"toolbox_back_column": [6.0, 1.0],
|
||||||
"toolbox_back_button": [6.0, 2.0],
|
"toolbox_back_button": [6.0, 2.0],
|
||||||
"toolbox_installed_tile": [1.0, 8.0],
|
"toolbox_installed_tile": [1.0, 8.0],
|
||||||
|
@ -603,7 +600,6 @@
|
||||||
"toolbox_heading_label": [1.0, 3.8],
|
"toolbox_heading_label": [1.0, 3.8],
|
||||||
"toolbox_header": [1.0, 4.0],
|
"toolbox_header": [1.0, 4.0],
|
||||||
"toolbox_header_highlight": [0.25, 0.25],
|
"toolbox_header_highlight": [0.25, 0.25],
|
||||||
"toolbox_progress_bar": [8.0, 0.5],
|
|
||||||
"toolbox_chart_row": [1.0, 2.0],
|
"toolbox_chart_row": [1.0, 2.0],
|
||||||
"toolbox_action_button": [8.0, 2.5],
|
"toolbox_action_button": [8.0, 2.5],
|
||||||
"toolbox_loader": [2.0, 2.0],
|
"toolbox_loader": [2.0, 2.0],
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue