mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-08-08 22:35:03 -06:00
Merge branch 'master' of github.com:Ultimaker/Cura into network_rewrite
This commit is contained in:
commit
aba8bd89c3
129 changed files with 15629 additions and 3612 deletions
|
@ -13,6 +13,7 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
|
|||
from UM.MimeTypeDatabase import MimeTypeDatabase
|
||||
from UM.Job import Job
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Util import parseBool
|
||||
from .WorkspaceDialog import WorkspaceDialog
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
@ -448,6 +449,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
extruder_stacks = []
|
||||
extruder_stacks_added = []
|
||||
container_stacks_added = []
|
||||
machine_extruder_count = None
|
||||
|
||||
containers_added = []
|
||||
|
||||
|
@ -635,6 +637,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
# The ID already exists, but nothing in the values changed, so do nothing.
|
||||
pass
|
||||
quality_and_definition_changes_instance_containers.append(instance_container)
|
||||
|
||||
if container_type == "definition_changes":
|
||||
definition_changes_extruder_count = instance_container.getProperty("machine_extruder_count", "value")
|
||||
if definition_changes_extruder_count is not None:
|
||||
machine_extruder_count = definition_changes_extruder_count
|
||||
|
||||
else:
|
||||
existing_container = self._container_registry.findInstanceContainers(id = container_id)
|
||||
if not existing_container:
|
||||
|
@ -714,11 +722,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
extruder_file_content = archive.open(extruder_stack_file, "r").read().decode("utf-8")
|
||||
|
||||
if self._resolve_strategies["machine"] == "override":
|
||||
if global_stack.getProperty("machine_extruder_count", "value") > 1:
|
||||
# deserialize new extruder stack over the current ones (if any)
|
||||
stack = self._overrideExtruderStack(global_stack, extruder_file_content, extruder_stack_file)
|
||||
if stack is None:
|
||||
continue
|
||||
# deserialize new extruder stack over the current ones (if any)
|
||||
stack = self._overrideExtruderStack(global_stack, extruder_file_content, extruder_stack_file)
|
||||
if stack is None:
|
||||
continue
|
||||
|
||||
elif self._resolve_strategies["machine"] == "new":
|
||||
new_id = extruder_stack_id_map[container_id]
|
||||
|
@ -759,7 +766,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
if not extruder_stacks:
|
||||
stack = self._container_registry.addExtruderStackForSingleExtrusionMachine(global_stack, "fdmextruder")
|
||||
if stack:
|
||||
extruder_stacks.append(stack)
|
||||
if self._resolve_strategies["machine"] == "override":
|
||||
# in case the extruder is newly created (for a single-extrusion machine), we need to override
|
||||
# the existing extruder stack.
|
||||
existing_extruder_stack = global_stack.extruders[stack.getMetaDataEntry("position")]
|
||||
for idx in range(len(_ContainerIndexes.IndexTypeMap)):
|
||||
existing_extruder_stack.replaceContainer(idx, stack._containers[idx], postpone_emit = True)
|
||||
extruder_stacks.append(existing_extruder_stack)
|
||||
else:
|
||||
extruder_stacks.append(stack)
|
||||
|
||||
except:
|
||||
Logger.logException("w", "We failed to serialize the stack. Trying to clean up.")
|
||||
|
@ -768,6 +783,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
self._container_registry.removeContainer(container.getId())
|
||||
return
|
||||
|
||||
|
||||
# Check quality profiles to make sure that if one stack has the "not supported" quality profile,
|
||||
# all others should have the same.
|
||||
#
|
||||
|
@ -782,56 +798,101 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
#
|
||||
has_not_supported = False
|
||||
for stack in [global_stack] + extruder_stacks:
|
||||
if stack.quality.getId() == "empty_quality":
|
||||
if stack.quality.getId() in ("empty", "empty_quality"):
|
||||
has_not_supported = True
|
||||
break
|
||||
|
||||
# We filter out extruder stacks that are not actually used, for example the UM3 and custom FDM printer extruder count setting.
|
||||
extruder_stacks_in_use = extruder_stacks
|
||||
if machine_extruder_count is not None:
|
||||
extruder_stacks_in_use = extruder_stacks[:machine_extruder_count]
|
||||
|
||||
available_quality = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_stack,
|
||||
extruder_stacks_in_use)
|
||||
if not has_not_supported:
|
||||
available_quality = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_stack, extruder_stacks)
|
||||
has_not_supported = not available_quality
|
||||
|
||||
quality_has_been_changed = False
|
||||
|
||||
if has_not_supported:
|
||||
empty_quality_container = self._container_registry.findInstanceContainers(id = "empty_quality")[0]
|
||||
for stack in [global_stack] + extruder_stacks:
|
||||
for stack in [global_stack] + extruder_stacks_in_use:
|
||||
stack.replaceContainer(_ContainerIndexes.Quality, empty_quality_container)
|
||||
empty_quality_changes_container = self._container_registry.findInstanceContainers(id = "empty_quality_changes")[0]
|
||||
for stack in [global_stack] + extruder_stacks_in_use:
|
||||
stack.replaceContainer(_ContainerIndexes.QualityChanges, empty_quality_changes_container)
|
||||
quality_has_been_changed = True
|
||||
|
||||
# Fix quality:
|
||||
# The quality specified in an old project file can be wrong, for example, for UM2, it should be "um2_normal"
|
||||
# but instead it was "normal". This should be fixed by setting it to the correct quality.
|
||||
# Note that this only seems to happen on single-extrusion machines on the global stack, so we only apply the
|
||||
# fix for that
|
||||
quality = global_stack.quality
|
||||
if quality.getId() not in ("empty", "empty_quality"):
|
||||
quality_type = quality.getMetaDataEntry("quality_type")
|
||||
quality_containers = self._container_registry.findInstanceContainers(definition = global_stack.definition.getId(),
|
||||
else:
|
||||
empty_quality_changes_container = self._container_registry.findInstanceContainers(id="empty_quality_changes")[0]
|
||||
|
||||
# The machine in the project has non-empty quality and there are usable qualities for this machine.
|
||||
# We need to check if the current quality_type is still usable for this machine, if not, then the quality
|
||||
# will be reset to the "preferred quality" if present, otherwise "normal".
|
||||
available_quality_types = [q.getMetaDataEntry("quality_type") for q in available_quality]
|
||||
|
||||
if global_stack.quality.getMetaDataEntry("quality_type") not in available_quality_types:
|
||||
# We are here because the quality_type specified in the project is not supported any more,
|
||||
# so we need to switch it to the "preferred quality" if present, otherwise "normal".
|
||||
quality_has_been_changed = True
|
||||
|
||||
# find the preferred quality
|
||||
preferred_quality_id = global_stack.getMetaDataEntry("preferred_quality", None)
|
||||
if preferred_quality_id is not None:
|
||||
definition_id = global_stack.definition.getId()
|
||||
definition_id = global_stack.definition.getMetaDataEntry("quality_definition", definition_id)
|
||||
if not parseBool(global_stack.getMetaDataEntry("has_machine_quality", "False")):
|
||||
definition_id = "fdmprinter"
|
||||
|
||||
containers = self._container_registry.findInstanceContainers(id = preferred_quality_id,
|
||||
type = "quality",
|
||||
quality_type = quality_type)
|
||||
quality_containers = [q for q in quality_containers if q.getMetaDataEntry("material", "") == ""]
|
||||
if quality_containers:
|
||||
global_stack.quality = quality_containers[0]
|
||||
else:
|
||||
# look for "fdmprinter" qualities if the machine-specific qualities cannot be found
|
||||
quality_containers = self._container_registry.findInstanceContainers(definition = "fdmprinter",
|
||||
type = "quality",
|
||||
quality_type = quality_type)
|
||||
quality_containers = [q for q in quality_containers if q.getMetaDataEntry("material", "") == ""]
|
||||
if quality_containers:
|
||||
global_stack.quality = quality_containers[0]
|
||||
else:
|
||||
# the quality_type of the quality profile cannot be found.
|
||||
# this can happen if a quality_type has been removed in a newer version, for example:
|
||||
# "extra_coarse" is removed from 2.7 to 3.0
|
||||
# in this case, the quality will be reset to "normal"
|
||||
quality_containers = self._container_registry.findInstanceContainers(
|
||||
definition = global_stack.definition.getId(),
|
||||
type = "quality",
|
||||
quality_type = "normal")
|
||||
quality_containers = [q for q in quality_containers if q.getMetaDataEntry("material", "") == ""]
|
||||
if quality_containers:
|
||||
global_stack.quality = quality_containers[0]
|
||||
definition = definition_id)
|
||||
containers = [c for c in containers if not c.getMetaDataEntry("material", "")]
|
||||
if containers:
|
||||
global_stack.quality = containers[0]
|
||||
global_stack.qualityChanges = empty_quality_changes_container
|
||||
# also find the quality containers for the extruders
|
||||
for extruder_stack in extruder_stacks_in_use:
|
||||
search_criteria = {"id": preferred_quality_id,
|
||||
"type": "quality",
|
||||
"definition": definition_id}
|
||||
if global_stack.getMetaDataEntry("has_machine_materials") and extruder_stack.material.getId() not in ("empty", "empty_material"):
|
||||
search_criteria["material"] = extruder_stack.material.getId()
|
||||
containers = self._container_registry.findInstanceContainers(**search_criteria)
|
||||
if containers:
|
||||
extruder_stack.quality = containers[0]
|
||||
extruder_stack.qualityChanges = empty_quality_changes_container
|
||||
else:
|
||||
Logger.log("e", "Cannot find preferred quality for extruder [%s].", extruder_stack.getId())
|
||||
|
||||
else:
|
||||
# This should not happen!
|
||||
Logger.log("e", "Cannot find quality normal for global stack [%s] [%s]",
|
||||
global_stack.getId(), global_stack.definition.getId())
|
||||
global_stack.quality = self._container_registry.findInstanceContainers(id = "empty_quality")[0]
|
||||
# we cannot find the preferred quality. THIS SHOULD NOT HAPPEN
|
||||
Logger.log("e", "Cannot find the preferred quality for machine [%s]", global_stack.getId())
|
||||
else:
|
||||
# The quality_type specified in the project file is usable, but the quality container itself may not
|
||||
# be correct. For example, for UM2, the quality container can be "draft" while it should be "um2_draft"
|
||||
# instead.
|
||||
# In this code branch, we try to fix those incorrect quality containers.
|
||||
#
|
||||
# ***IMPORTANT***: We only do this fix for single-extrusion machines.
|
||||
# We will first find the correct quality profile for the extruder, then apply the same
|
||||
# quality profile for the global stack.
|
||||
#
|
||||
if len(extruder_stacks) == 1:
|
||||
extruder_stack = extruder_stacks[0]
|
||||
|
||||
search_criteria = {"type": "quality", "quality_type": global_stack.quality.getMetaDataEntry("quality_type")}
|
||||
search_criteria["definition"] = global_stack.definition.getId()
|
||||
if not parseBool(global_stack.getMetaDataEntry("has_machine_quality", "False")):
|
||||
search_criteria["definition"] = "fdmprinter"
|
||||
|
||||
if global_stack.getMetaDataEntry("has_machine_materials") and extruder_stack.material.getId() not in ("empty", "empty_material"):
|
||||
search_criteria["material"] = extruder_stack.material.getId()
|
||||
containers = self._container_registry.findInstanceContainers(**search_criteria)
|
||||
if containers:
|
||||
new_quality_container = containers[0]
|
||||
extruder_stack.quality = new_quality_container
|
||||
global_stack.quality = new_quality_container
|
||||
|
||||
# Replacing the old containers if resolve is "new".
|
||||
# When resolve is "new", some containers will get renamed, so all the other containers that reference to those
|
||||
|
@ -855,7 +916,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
global_stack.userChanges = container
|
||||
continue
|
||||
|
||||
for changes_container_type in ("quality_changes", "definition_changes"):
|
||||
changes_container_types = ("quality_changes", "definition_changes")
|
||||
if quality_has_been_changed:
|
||||
# DO NOT replace quality_changes if the current quality_type is not supported
|
||||
changes_container_types = ("definition_changes",)
|
||||
for changes_container_type in changes_container_types:
|
||||
if self._resolve_strategies[changes_container_type] == "new":
|
||||
# Quality changes needs to get a new ID, added to registry and to the right stacks
|
||||
for each_changes_container in quality_and_definition_changes_instance_containers:
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QUrl, pyqtSignal, QObject, pyqtProperty, QCoreApplication
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from PyQt5.QtQml import QQmlComponent, QQmlContext
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
|
||||
|
|
|
@ -8,8 +8,7 @@ from UM.Application import Application
|
|||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Version import Version
|
||||
|
||||
from PyQt5.QtQml import QQmlComponent, QQmlContext
|
||||
from PyQt5.QtCore import QUrl, pyqtSlot, QObject
|
||||
from PyQt5.QtCore import pyqtSlot, QObject
|
||||
|
||||
import os.path
|
||||
import collections
|
||||
|
|
|
@ -1,3 +1,72 @@
|
|||
[3.1.0]
|
||||
*Profile added for 0.25 mm print core
|
||||
This new print core gives extra fine line widths which gives prints extra definition and surface quality.
|
||||
|
||||
*Profile added for Breakaway material
|
||||
New material profile for Breakaway material, a new dry post processing support material, which can be used for models with flat surface area overhangs.
|
||||
|
||||
*Layer view
|
||||
The existing Layer View has been updated in order to see a live simulation of all the paths within a layer.
|
||||
|
||||
*Quick camera controls
|
||||
New buttons have been added to the interface that can quickly reposition the camera view of the buildplate.
|
||||
|
||||
*Lock model on platform
|
||||
The move tool has a new option to lock a selected model to the platform.
|
||||
|
||||
*Faster profile switching speed
|
||||
Duplicating and removing a profile could take Ultimaker Cura quite some time, it now happens instantly.
|
||||
|
||||
*Faster printer selection
|
||||
Removing a printer from the library is now instant. No more unresponsive screens.
|
||||
|
||||
*Faster processing speed
|
||||
A 5 - 10 % speed increase when calculating normals, loading models, and slicing.
|
||||
|
||||
*Feedrate visualization
|
||||
Feedrate visualization has been added to the Layer view. Using this gives the user an idea of the print speeds per model part, allowing for better control over prints.
|
||||
|
||||
*Jogging
|
||||
It allows the printhead to be moved with on-screen controls. Contributed by fieldOfView.
|
||||
|
||||
*Large model loading
|
||||
A new feature has been added which unloads the layer view when switching to solid mode, speeding Ultimaker Cura back up without losing your G-code/layer view information.
|
||||
|
||||
*Scripts folder
|
||||
A scripts folder is now available in the Ultimaker Cura configuration folder. This folder can be loaded with post processing plugins scripts, which will automatically show in Ultimaker Cura. Contributed by fieldOfView.
|
||||
|
||||
*Optimized workflow for crash reporting
|
||||
Crash reports are automatically generated and allow the user, in case of a crash, to easily send their report with a description to developers.
|
||||
|
||||
*Floating models enabled
|
||||
In previous releases, models were dropped to the build plate when support was disabled. Models now float when the setting is enabled (even if creates an impossible-to-print situation). This can be used to stack separate models on top of each other.
|
||||
|
||||
*Slicing tolerance
|
||||
A new setting that affects the intersect point to influence the dimensional accuracy for diagonal surfaces. The user can select the behaviour: ‘Inclusive’ makes gaps narrower, ‘Exclusive’ makes gaps wider, and ‘Middle’ is the fastest to process. This can be used to create better tolerances for printed screw holes. Contributed by BagelOrb.
|
||||
|
||||
*Optimized zig zag patterns
|
||||
Zig zag patterns now print more consistently. Lines now have a 5 micron tolerance in which they are printed any way, resulting in longer connected lines. Contributed by smartavionics.
|
||||
|
||||
*Aligned z-seam inner wall moves
|
||||
Inner wall travel moves are aligned with the z-seam. This reduces the number of travel moves and reduces the chance of more unwanted seams.
|
||||
|
||||
*Relative positioning of infill patterns
|
||||
Infill patterns are now positioned relative to the center of loaded models and an offset can be applied to control the infill more precisely and adjust it to preference or strength. Contributed by smartavionics.
|
||||
|
||||
*Line resolution
|
||||
Enables the user to specify the minimum allowed distance value between two points in G-code to create lower or higher resolution polygons.
|
||||
|
||||
*Custom mode changes
|
||||
If profile settings have been modified in recommended mode under custom mode, a reset icon will appear to notify the user. Click the icon to show the changes that have been made, and revert back to the default profile settings.
|
||||
|
||||
*Bugfixes
|
||||
- Fix for layer numbers being displayed incorrectly when switching between solid and layer mode
|
||||
- Fix for Ultimaker Cura engine crashes on certain models
|
||||
- Fix for Uninstalling previous versions of Cura on Windows platforms
|
||||
- Fix for displaying visible settings
|
||||
- Fix for importing legacy .ini files
|
||||
- Prevent skipping user agreement dialog by pressing escape
|
||||
|
||||
[3.0.4]
|
||||
*Bug fixes
|
||||
- Fixed OpenGL issue that prevents Cura from starting.
|
||||
|
@ -40,7 +109,7 @@ The build plate now shows graduations of 10 mm and 1 mm for easy model positioni
|
|||
Extruder tabs have become buttons and icons have been updated.
|
||||
|
||||
*Add an "Export to Cura" button in SOLIDWORKS
|
||||
SOLIDWORKS plugin can now be installed using an automatic installer.
|
||||
A macro can be added to your SOLIDWORKS installation that loads your model into Ultimaker Cura.
|
||||
|
||||
*Siemens NX macro
|
||||
When a user updates models in Siemens NX and clicks the button, the updated models replace the models opened in Ultimaker Cura.
|
||||
|
@ -571,10 +640,10 @@ The new GUI allows custom profiles to load easily and intuitively, directly from
|
|||
*3MF File Loading Support
|
||||
We’re happy to report we now support loading 3MF files. This is a new file format similar to AMF, but freely available.
|
||||
|
||||
*Intuitive Cut-Off Object Bottom
|
||||
*Intuitive Cut-Off Object Bottom
|
||||
We’ve added a feature that allows you to move objects below the build plate. You can either correct a model with a rough bottom, or print only a part of an object. Please note that the implementation differs greatly from the old one when it was just a setting.
|
||||
|
||||
*64-bit Windows Builds
|
||||
*64-bit Windows Builds
|
||||
An optimized 64-bit Windows Cura version is now available. This allows you to load larger model files.
|
||||
|
||||
*Automatic calculations
|
||||
|
@ -639,3 +708,165 @@ Prints the outer walls with a jittering motion to give your object a diffuse fin
|
|||
|
||||
*Wire Printing
|
||||
The object is printed with a mid-air / net-like structure, following the mesh surface. The build plate will move up and down during diagonal segments. Though not visible in layer view, you can view the result in other software, such as Repetier Host or http://chilipeppr.com/tinyg.
|
||||
|
||||
|
||||
[15.06 Beta]
|
||||
|
||||
Cura 15.06 is a new release built from the ground up on a completely new
|
||||
framework called Uranium. This framework has been designed to make it easier to
|
||||
extend Cura with additional functionality as well as provide a cleaner UI.
|
||||
|
||||
[15.05.95]
|
||||
|
||||
* Fixed: Selection ghost remains visible after deleting an object
|
||||
* Fixed: Window does not show up immediately after starting application on OSX
|
||||
* Fixed: Added display of rotation angle during rotation
|
||||
* Fixed: Object changes position while rotating/scaling
|
||||
* Fixed: Loading improvements in the layer view
|
||||
* Fixed: Added application icons
|
||||
* Fixed: Improved feedback when loading models
|
||||
* Fixed: Eject device on MacOSX now provides proper feedback
|
||||
* Fixed: Make it possible to show retraction settings for UM2
|
||||
* Fixed: Opening the machine preferences page will switch to the first available machine
|
||||
* Fixed: Improved tool handle hit area size
|
||||
* Fixed: Render lines with a thickness based on screen DPI
|
||||
|
||||
[15.05.94]
|
||||
|
||||
* Added Russian translations
|
||||
* Fixed: Infill not displayed in layer view
|
||||
* Fixed: Cannot select/scale/rotate when first activating the tool and then trying to select a model.
|
||||
* Fixed: Improved font rendering on Windows
|
||||
* Fixed: Help > Show Documentation crashes Cura on Windows
|
||||
* Fixed: "There is no disk in the drive" repeating messages on Windows
|
||||
* Fixed: Retraction settings not visible for Ultimaker2
|
||||
* Fixed: Display rotation angle when rotating an object
|
||||
* Fixed: Time/Quality slider values are properly rounded
|
||||
* Fixed: Improved clarity of buttons and text
|
||||
* Fixed: No indication that anything is happening when loading a model
|
||||
* Fixed: Eject device now works on Windows
|
||||
|
||||
[15.05.93]
|
||||
|
||||
* Fixed: No shortcuts for moving up/down layers in layer view.
|
||||
* Fixed: Last view layers could not be scrolled through in layer view.
|
||||
* Fixed: Files provided on command line would not actually show up on the build
|
||||
platform.
|
||||
* Fixed: Render a ghost of the selection in Layer view to make the actual object
|
||||
position clear.
|
||||
* Fixed: Showing a menu would clear the selection.
|
||||
* Fixed: Size and scaling factor display for scale tool.
|
||||
* Fixed: Missing background for additional tool controls.
|
||||
* Fixed: Loading message times out when loading large files.
|
||||
* Fixed: Show recent files in the file menu.
|
||||
* Fixed: Windows installer will now install MSVC 2010 redistributable, to
|
||||
prevent issues with missing DLL's.
|
||||
* Fixed: Collapsed/expanded state of setting categories not stored.
|
||||
|
||||
[15.05.91]
|
||||
|
||||
* There is now a working MacOSX version. Currently it supports OSX 10.7 and
|
||||
higher.
|
||||
* Fixed: Need to deselect before selecting a different object.
|
||||
* Fixed: Object can be moved on Z axis.
|
||||
* Fixed: Error values should be considered invalid values and will not trigger a
|
||||
slice.
|
||||
* Fixed: Text fields used a locale-aware validator while the underlying code did
|
||||
not.
|
||||
* Fixed: Text fields will trigger a slice on text change, not only after focus
|
||||
change/enter press.
|
||||
* Fixed: Rotate Tool snaps to incorrect value.
|
||||
* Fixed: Object Collision would only moved objects to the right.
|
||||
* Fixed: Object Collision would move the selected object when it should not.
|
||||
* Fixed: Camera panning now works correctly instead of doing nothing.
|
||||
* Fixed: Camera would flip around center point at maximum rotation.
|
||||
* Fixed: Build platform grid blocked view from below objects.
|
||||
* Fixed: Viewport on MacOSX with high-DPI screens was only taking 1/4th of the
|
||||
window
|
||||
|
||||
[15.05.90]
|
||||
|
||||
* Fixed: Additional UI elements for tools and views not loading.
|
||||
* Fixed: Double click needed to change setting dialog page.
|
||||
* Fixed: Context menu entries (reload, center object, etc.) not working.
|
||||
* Fixed: "Open With" or passing files from command line not working.
|
||||
* Fixed: "Reload All" would not reload files.
|
||||
|
||||
In addition, a lot of work has gone into getting a usable Mac OSX version.
|
||||
|
||||
New Features
|
||||
------------
|
||||
|
||||
* Plugin based system
|
||||
The Uranium framework provides us with a plugin-based system
|
||||
that provides additional flexibility when extending Cura. Think
|
||||
of new views, tools, file formats, etc. This is probably the
|
||||
biggest new feature.
|
||||
* Improved UI
|
||||
The UI has received a complete overhaul.
|
||||
* Time-Quality Slider
|
||||
The 4 static quick print profiles have been replaced with
|
||||
a slider that should make it easier to find the right spot
|
||||
between print time and print quality.
|
||||
* More Settings
|
||||
The Advanced mode is now configurable and can show many
|
||||
additional settings that were previously not available, while at
|
||||
the same time not overwhelming new users with too many settings.
|
||||
Custom set of visible settings can be created by the user.
|
||||
* Support for high-DPI screens
|
||||
The refreshed UI has been designed with high-DPI screens in
|
||||
mind which should improve the experience of Cura on such
|
||||
devices.
|
||||
* Improved language support
|
||||
(Not yet available for the Beta release.)
|
||||
* Improved support structure generation
|
||||
The new version of the CuraEngine now features improved
|
||||
support generation algorithms and additional options for support
|
||||
structure generation.
|
||||
* Experimental Feature: Wire Printing
|
||||
Wire Printing has been added as an experimental new feature. It
|
||||
will print objects as a structure of lines. It can be enabled by
|
||||
from Advanced Mode -> Fixes -> Wire Printing.
|
||||
* Undo/Redo
|
||||
It is now possible to undo and redo most scene operations, like
|
||||
moving or rotating objects.
|
||||
|
||||
Features from earlier versions not (yet) in this release
|
||||
--------------------------------------------------------
|
||||
|
||||
* The All-at-once/One-at-a-time toggle is not available.
|
||||
We are working on an improved implementation of this mechanism
|
||||
but it will not be available for this release.
|
||||
* No dual extrusion features are available yet.
|
||||
We are working on a completely new workflow for this but this
|
||||
needs additional time.
|
||||
* “Lay Flat” has been removed.
|
||||
The existing implementation was unfortunately not salvageable.
|
||||
We will be looking into an improved implementation for this
|
||||
feature.
|
||||
* "Split Object Into Parts" has been removed.
|
||||
Due to the same reason as Lay Flat.
|
||||
* Support for AMF and DAE file formats has been removed.
|
||||
Both of these will be implemented as plugins in the future.
|
||||
* Support for directly loading a GCode file is not yet available.
|
||||
This will be implemented as a plugin in the future.
|
||||
* Support for PNG, JPG and other image formats has been removed.
|
||||
These can be supported by a plugin with an improved UI.
|
||||
* Support for loading Minecraft levels has been removed.
|
||||
This can be implemented as a plugin.
|
||||
* Windows XP support has been dropped.
|
||||
Microsoft is no longer supporting xp, so they no longer back
|
||||
port certain features that we require.
|
||||
* X-Ray view is missing.
|
||||
Will be implemented as a (you might have guessed it) plugin.
|
||||
* Fixes: Follow Mesh Surface
|
||||
Has been removed from the engine, the same result can be
|
||||
achieved using no infill or top/bottom layers.
|
||||
|
||||
Known Issues
|
||||
------------
|
||||
* Some OBJ files are rendered as black objects due to missing
|
||||
normals.
|
||||
* Disabling plugins does not work correctly yet.
|
||||
* Unicorn occasionally still requires feeding. Do not feed it
|
||||
after midnight.
|
||||
|
|
|
@ -61,7 +61,9 @@ class ProcessSlicedLayersJob(Job):
|
|||
|
||||
def run(self):
|
||||
start_time = time()
|
||||
if Application.getInstance().getController().getActiveView().getPluginId() == "SimulationView":
|
||||
view = Application.getInstance().getController().getActiveView()
|
||||
if view.getPluginId() == "SimulationView":
|
||||
view.resetLayerData()
|
||||
self._progress_message.show()
|
||||
Job.yieldThread()
|
||||
if self._abort_requested:
|
||||
|
@ -226,10 +228,6 @@ class ProcessSlicedLayersJob(Job):
|
|||
if self._progress_message:
|
||||
self._progress_message.setProgress(100)
|
||||
|
||||
view = Application.getInstance().getController().getActiveView()
|
||||
if view.getPluginId() == "SimulationView":
|
||||
view.resetLayerData()
|
||||
|
||||
if self._progress_message:
|
||||
self._progress_message.hide()
|
||||
|
||||
|
|
|
@ -45,14 +45,6 @@ class GcodeStartEndFormatter(Formatter):
|
|||
|
||||
## Job class that builds up the message of scene data to send to CuraEngine.
|
||||
class StartSliceJob(Job):
|
||||
## Meshes that are sent to the engine regardless of being outside of the
|
||||
# build volume.
|
||||
#
|
||||
# If these settings are True for any mesh, the build volume is ignored.
|
||||
# Note that Support Mesh is not in here because it actually generates
|
||||
# g-code in the volume of the mesh.
|
||||
_not_printed_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"}
|
||||
|
||||
def __init__(self, slice_message):
|
||||
super().__init__()
|
||||
|
||||
|
@ -141,8 +133,7 @@ class StartSliceJob(Job):
|
|||
temp_list = []
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
|
||||
if not getattr(node, "_outside_buildarea", False)\
|
||||
or (node.callDecoration("getStack") and any(node.callDecoration("getStack").getProperty(setting, "value") for setting in self._not_printed_mesh_settings)):
|
||||
if not getattr(node, "_outside_buildarea", False) or getattr(node, "_non_printing_mesh", False):
|
||||
temp_list.append(node)
|
||||
Job.yieldThread()
|
||||
|
||||
|
@ -205,32 +196,53 @@ class StartSliceJob(Job):
|
|||
def isCancelled(self):
|
||||
return self._is_cancelled
|
||||
|
||||
def _expandGcodeTokens(self, key, value, settings):
|
||||
## Creates a dictionary of tokens to replace in g-code pieces.
|
||||
#
|
||||
# This indicates what should be replaced in the start and end g-codes.
|
||||
# \param stack The stack to get the settings from to replace the tokens
|
||||
# with.
|
||||
# \return A dictionary of replacement tokens to the values they should be
|
||||
# replaced with.
|
||||
def _buildReplacementTokens(self, stack) -> dict:
|
||||
result = {}
|
||||
for key in stack.getAllKeys():
|
||||
result[key] = stack.getProperty(key, "value")
|
||||
Job.yieldThread()
|
||||
|
||||
result["print_bed_temperature"] = result["material_bed_temperature"] #Renamed settings.
|
||||
result["print_temperature"] = result["material_print_temperature"]
|
||||
result["time"] = time.strftime("%H:%M:%S") #Some extra settings.
|
||||
result["date"] = time.strftime("%d-%m-%Y")
|
||||
result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))]
|
||||
|
||||
return result
|
||||
|
||||
## Replace setting tokens in a piece of g-code.
|
||||
# \param value A piece of g-code to replace tokens in.
|
||||
# \param settings A dictionary of tokens to replace and their respective
|
||||
# replacement strings.
|
||||
def _expandGcodeTokens(self, value: str, settings: dict):
|
||||
try:
|
||||
# any setting can be used as a token
|
||||
fmt = GcodeStartEndFormatter()
|
||||
return str(fmt.format(value, **settings)).encode("utf-8")
|
||||
return str(fmt.format(value, **settings))
|
||||
except:
|
||||
Logger.logException("w", "Unable to do token replacement on start/end gcode")
|
||||
return str(value).encode("utf-8")
|
||||
return str(value)
|
||||
|
||||
## Create extruder message from stack
|
||||
def _buildExtruderMessage(self, stack):
|
||||
message = self._slice_message.addRepeatedMessage("extruders")
|
||||
message.id = int(stack.getMetaDataEntry("position"))
|
||||
|
||||
material_instance_container = stack.findContainer({"type": "material"})
|
||||
settings = self._buildReplacementTokens(stack)
|
||||
|
||||
settings = {}
|
||||
for key in stack.getAllKeys():
|
||||
settings[key] = stack.getProperty(key, "value")
|
||||
Job.yieldThread()
|
||||
#Also send the material GUID. This is a setting in fdmprinter, but we have no interface for it.
|
||||
settings["material_guid"] = stack.material.getMetaDataEntry("GUID", "")
|
||||
|
||||
settings["print_bed_temperature"] = settings["material_bed_temperature"] #Renamed settings.
|
||||
settings["print_temperature"] = settings["material_print_temperature"]
|
||||
settings["time"] = time.strftime("%H:%M:%S") #Some extra settings.
|
||||
settings["date"] = time.strftime("%d-%m-%Y")
|
||||
settings["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))]
|
||||
#Replace the setting tokens in start and end g-code.
|
||||
settings["machine_extruder_start_code"] = self._expandGcodeTokens(settings["machine_extruder_start_code"], settings)
|
||||
settings["machine_extruder_end_code"] = self._expandGcodeTokens(settings["machine_extruder_end_code"], settings)
|
||||
|
||||
for key, value in settings.items():
|
||||
# Do not send settings that are not settable_per_extruder.
|
||||
|
@ -238,13 +250,7 @@ class StartSliceJob(Job):
|
|||
continue
|
||||
setting = message.getMessage("settings").addRepeatedMessage("settings")
|
||||
setting.name = key
|
||||
if key == "material_guid" and material_instance_container:
|
||||
# Also send the material GUID. This is a setting in fdmprinter, but we have no interface for it.
|
||||
setting.value = str(material_instance_container.getMetaDataEntry("GUID", "")).encode("utf-8")
|
||||
elif key == "machine_extruder_start_code" or key == "machine_extruder_end_code":
|
||||
setting.value = self._expandGcodeTokens(key, value, settings)
|
||||
else:
|
||||
setting.value = str(stack.getProperty(key, "value")).encode("utf-8")
|
||||
setting.value = str(value).encode("utf-8")
|
||||
Job.yieldThread()
|
||||
|
||||
## Sends all global settings to the engine.
|
||||
|
@ -252,11 +258,7 @@ class StartSliceJob(Job):
|
|||
# The settings are taken from the global stack. This does not include any
|
||||
# per-extruder settings or per-object settings.
|
||||
def _buildGlobalSettingsMessage(self, stack):
|
||||
keys = stack.getAllKeys()
|
||||
settings = {}
|
||||
for key in keys:
|
||||
settings[key] = stack.getProperty(key, "value")
|
||||
Job.yieldThread()
|
||||
settings = self._buildReplacementTokens(stack)
|
||||
|
||||
start_gcode = settings["machine_start_gcode"]
|
||||
#Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
|
||||
|
@ -265,20 +267,14 @@ class StartSliceJob(Job):
|
|||
print_temperature_settings = {"material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"}
|
||||
settings["material_print_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in print_temperature_settings))
|
||||
|
||||
settings["print_bed_temperature"] = settings["material_bed_temperature"]
|
||||
settings["print_temperature"] = settings["material_print_temperature"]
|
||||
|
||||
settings["time"] = time.strftime('%H:%M:%S')
|
||||
settings["date"] = time.strftime('%d-%m-%Y')
|
||||
settings["day"] = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][int(time.strftime('%w'))]
|
||||
#Replace the setting tokens in start and end g-code.
|
||||
settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], settings)
|
||||
settings["machine_end_gcode"] = self._expandGcodeTokens(settings["machine_end_gcode"], settings)
|
||||
|
||||
for key, value in settings.items(): #Add all submessages for each individual setting.
|
||||
setting_message = self._slice_message.getMessage("global_settings").addRepeatedMessage("settings")
|
||||
setting_message.name = key
|
||||
if key == "machine_start_gcode" or key == "machine_end_gcode": #If it's a g-code message, use special formatting.
|
||||
setting_message.value = self._expandGcodeTokens(key, value, settings)
|
||||
else:
|
||||
setting_message.value = str(value).encode("utf-8")
|
||||
setting_message.value = str(value).encode("utf-8")
|
||||
Job.yieldThread()
|
||||
|
||||
## Sends for some settings which extruder they should fallback to if not
|
||||
|
@ -348,3 +344,4 @@ class StartSliceJob(Job):
|
|||
|
||||
relations_set.add(relation.target.key)
|
||||
self._addRelations(relations_set, relation.target.relations)
|
||||
|
||||
|
|
462
plugins/GCodeReader/FlavorParser.py
Normal file
462
plugins/GCodeReader/FlavorParser.py
Normal file
|
@ -0,0 +1,462 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Backend import Backend
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Message import Message
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Preferences import Preferences
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
from cura import LayerDataBuilder
|
||||
from cura import LayerDataDecorator
|
||||
from cura.LayerPolygon import LayerPolygon
|
||||
from cura.GCodeListDecorator import GCodeListDecorator
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
import numpy
|
||||
import math
|
||||
import re
|
||||
from collections import namedtuple
|
||||
|
||||
# This parser is intented for interpret the common firmware codes among all the different flavors
|
||||
class FlavorParser:
|
||||
|
||||
def __init__(self):
|
||||
Application.getInstance().hideMessageSignal.connect(self._onHideMessage)
|
||||
self._cancelled = False
|
||||
self._message = None
|
||||
self._layer_number = 0
|
||||
self._extruder_number = 0
|
||||
self._clearValues()
|
||||
self._scene_node = None
|
||||
# X, Y, Z position, F feedrate and E extruder values are stored
|
||||
self._position = namedtuple('Position', ['x', 'y', 'z', 'f', 'e'])
|
||||
self._is_layers_in_file = False # Does the Gcode have the layers comment?
|
||||
self._extruder_offsets = {} # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
|
||||
self._current_layer_thickness = 0.2 # default
|
||||
|
||||
Preferences.getInstance().addPreference("gcodereader/show_caution", True)
|
||||
|
||||
def _clearValues(self):
|
||||
self._filament_diameter = 2.85
|
||||
self._extruder_number = 0
|
||||
self._extrusion_length_offset = [0]
|
||||
self._layer_type = LayerPolygon.Inset0Type
|
||||
self._layer_number = 0
|
||||
self._previous_z = 0
|
||||
self._layer_data_builder = LayerDataBuilder.LayerDataBuilder()
|
||||
self._center_is_zero = False
|
||||
self._is_absolute_positioning = True # It can be absolute (G90) or relative (G91)
|
||||
self._is_absolute_extrusion = True # It can become absolute (M82, default) or relative (M83)
|
||||
|
||||
@staticmethod
|
||||
def _getValue(line, code):
|
||||
n = line.find(code)
|
||||
if n < 0:
|
||||
return None
|
||||
n += len(code)
|
||||
pattern = re.compile("[;\s]")
|
||||
match = pattern.search(line, n)
|
||||
m = match.start() if match is not None else -1
|
||||
try:
|
||||
if m < 0:
|
||||
return line[n:]
|
||||
return line[n:m]
|
||||
except:
|
||||
return None
|
||||
|
||||
def _getInt(self, line, code):
|
||||
value = self._getValue(line, code)
|
||||
try:
|
||||
return int(value)
|
||||
except:
|
||||
return None
|
||||
|
||||
def _getFloat(self, line, code):
|
||||
value = self._getValue(line, code)
|
||||
try:
|
||||
return float(value)
|
||||
except:
|
||||
return None
|
||||
|
||||
def _onHideMessage(self, message):
|
||||
if message == self._message:
|
||||
self._cancelled = True
|
||||
|
||||
@staticmethod
|
||||
def _getNullBoundingBox():
|
||||
return AxisAlignedBox(minimum=Vector(0, 0, 0), maximum=Vector(10, 10, 10))
|
||||
|
||||
def _createPolygon(self, layer_thickness, path, extruder_offsets):
|
||||
countvalid = 0
|
||||
for point in path:
|
||||
if point[5] > 0:
|
||||
countvalid += 1
|
||||
if countvalid >= 2:
|
||||
# we know what to do now, no need to count further
|
||||
continue
|
||||
if countvalid < 2:
|
||||
return False
|
||||
try:
|
||||
self._layer_data_builder.addLayer(self._layer_number)
|
||||
self._layer_data_builder.setLayerHeight(self._layer_number, path[0][2])
|
||||
self._layer_data_builder.setLayerThickness(self._layer_number, layer_thickness)
|
||||
this_layer = self._layer_data_builder.getLayer(self._layer_number)
|
||||
except ValueError:
|
||||
return False
|
||||
count = len(path)
|
||||
line_types = numpy.empty((count - 1, 1), numpy.int32)
|
||||
line_widths = numpy.empty((count - 1, 1), numpy.float32)
|
||||
line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
|
||||
line_feedrates = numpy.empty((count - 1, 1), numpy.float32)
|
||||
line_widths[:, 0] = 0.35 # Just a guess
|
||||
line_thicknesses[:, 0] = layer_thickness
|
||||
points = numpy.empty((count, 3), numpy.float32)
|
||||
extrusion_values = numpy.empty((count, 1), numpy.float32)
|
||||
i = 0
|
||||
for point in path:
|
||||
points[i, :] = [point[0] + extruder_offsets[0], point[2], -point[1] - extruder_offsets[1]]
|
||||
extrusion_values[i] = point[4]
|
||||
if i > 0:
|
||||
line_feedrates[i - 1] = point[3]
|
||||
line_types[i - 1] = point[5]
|
||||
if point[5] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]:
|
||||
line_widths[i - 1] = 0.1
|
||||
line_thicknesses[i - 1] = 0.0 # Travels are set as zero thickness lines
|
||||
else:
|
||||
line_widths[i - 1] = self._calculateLineWidth(points[i], points[i-1], extrusion_values[i], extrusion_values[i-1], layer_thickness)
|
||||
i += 1
|
||||
|
||||
this_poly = LayerPolygon(self._extruder_number, line_types, points, line_widths, line_thicknesses, line_feedrates)
|
||||
this_poly.buildCache()
|
||||
|
||||
this_layer.polygons.append(this_poly)
|
||||
return True
|
||||
|
||||
def _createEmptyLayer(self, layer_number):
|
||||
self._layer_data_builder.addLayer(layer_number)
|
||||
self._layer_data_builder.setLayerHeight(layer_number, 0)
|
||||
self._layer_data_builder.setLayerThickness(layer_number, 0)
|
||||
|
||||
def _calculateLineWidth(self, current_point, previous_point, current_extrusion, previous_extrusion, layer_thickness):
|
||||
# Area of the filament
|
||||
Af = (self._filament_diameter / 2) ** 2 * numpy.pi
|
||||
# Length of the extruded filament
|
||||
de = current_extrusion - previous_extrusion
|
||||
# Volumne of the extruded filament
|
||||
dVe = de * Af
|
||||
# Length of the printed line
|
||||
dX = numpy.sqrt((current_point[0] - previous_point[0])**2 + (current_point[2] - previous_point[2])**2)
|
||||
# When the extruder recovers from a retraction, we get zero distance
|
||||
if dX == 0:
|
||||
return 0.1
|
||||
# Area of the printed line. This area is a rectangle
|
||||
Ae = dVe / dX
|
||||
# This area is a rectangle with area equal to layer_thickness * layer_width
|
||||
line_width = Ae / layer_thickness
|
||||
|
||||
# A threshold is set to avoid weird paths in the GCode
|
||||
if line_width > 1.2:
|
||||
return 0.35
|
||||
return line_width
|
||||
|
||||
def _gCode0(self, position, params, path):
|
||||
x, y, z, f, e = position
|
||||
|
||||
if self._is_absolute_positioning:
|
||||
x = params.x if params.x is not None else x
|
||||
y = params.y if params.y is not None else y
|
||||
z = params.z if params.z is not None else z
|
||||
else:
|
||||
x += params.x if params.x is not None else 0
|
||||
y += params.y if params.y is not None else 0
|
||||
z += params.z if params.z is not None else 0
|
||||
|
||||
f = params.f if params.f is not None else f
|
||||
|
||||
if params.e is not None:
|
||||
new_extrusion_value = params.e if self._is_absolute_extrusion else e[self._extruder_number] + params.e
|
||||
if new_extrusion_value > e[self._extruder_number]:
|
||||
path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], self._layer_type]) # extrusion
|
||||
else:
|
||||
path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) # retraction
|
||||
e[self._extruder_number] = new_extrusion_value
|
||||
|
||||
# Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
|
||||
# Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
|
||||
if z > self._previous_z and (z - self._previous_z < 1.5):
|
||||
self._current_layer_thickness = z - self._previous_z # allow a tiny overlap
|
||||
self._previous_z = z
|
||||
else:
|
||||
path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType])
|
||||
return self._position(x, y, z, f, e)
|
||||
|
||||
|
||||
# G0 and G1 should be handled exactly the same.
|
||||
_gCode1 = _gCode0
|
||||
|
||||
## Home the head.
|
||||
def _gCode28(self, position, params, path):
|
||||
return self._position(
|
||||
params.x if params.x is not None else position.x,
|
||||
params.y if params.y is not None else position.y,
|
||||
params.z if params.z is not None else position.z,
|
||||
position.f,
|
||||
position.e)
|
||||
|
||||
## Set the absolute positioning
|
||||
def _gCode90(self, position, params, path):
|
||||
self._is_absolute_positioning = True
|
||||
self._is_absolute_extrusion = True
|
||||
return position
|
||||
|
||||
## Set the relative positioning
|
||||
def _gCode91(self, position, params, path):
|
||||
self._is_absolute_positioning = False
|
||||
self._is_absolute_extrusion = False
|
||||
return position
|
||||
|
||||
## Reset the current position to the values specified.
|
||||
# For example: G92 X10 will set the X to 10 without any physical motion.
|
||||
def _gCode92(self, position, params, path):
|
||||
if params.e is not None:
|
||||
# Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
|
||||
self._extrusion_length_offset[self._extruder_number] += position.e[self._extruder_number] - params.e
|
||||
position.e[self._extruder_number] = params.e
|
||||
return self._position(
|
||||
params.x if params.x is not None else position.x,
|
||||
params.y if params.y is not None else position.y,
|
||||
params.z if params.z is not None else position.z,
|
||||
params.f if params.f is not None else position.f,
|
||||
position.e)
|
||||
|
||||
def processGCode(self, G, line, position, path):
|
||||
func = getattr(self, "_gCode%s" % G, None)
|
||||
line = line.split(";", 1)[0] # Remove comments (if any)
|
||||
if func is not None:
|
||||
s = line.upper().split(" ")
|
||||
x, y, z, f, e = None, None, None, None, None
|
||||
for item in s[1:]:
|
||||
if len(item) <= 1:
|
||||
continue
|
||||
if item.startswith(";"):
|
||||
continue
|
||||
if item[0] == "X":
|
||||
x = float(item[1:])
|
||||
if item[0] == "Y":
|
||||
y = float(item[1:])
|
||||
if item[0] == "Z":
|
||||
z = float(item[1:])
|
||||
if item[0] == "F":
|
||||
f = float(item[1:]) / 60
|
||||
if item[0] == "E":
|
||||
e = float(item[1:])
|
||||
if self._is_absolute_positioning and ((x is not None and x < 0) or (y is not None and y < 0)):
|
||||
self._center_is_zero = True
|
||||
params = self._position(x, y, z, f, e)
|
||||
return func(position, params, path)
|
||||
return position
|
||||
|
||||
def processTCode(self, T, line, position, path):
|
||||
self._extruder_number = T
|
||||
if self._extruder_number + 1 > len(position.e):
|
||||
self._extrusion_length_offset.extend([0] * (self._extruder_number - len(position.e) + 1))
|
||||
position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
|
||||
return position
|
||||
|
||||
def processMCode(self, M, line, position, path):
|
||||
pass
|
||||
|
||||
_type_keyword = ";TYPE:"
|
||||
_layer_keyword = ";LAYER:"
|
||||
|
||||
## For showing correct x, y offsets for each extruder
|
||||
def _extruderOffsets(self):
|
||||
result = {}
|
||||
for extruder in ExtruderManager.getInstance().getExtruderStacks():
|
||||
result[int(extruder.getMetaData().get("position", "0"))] = [
|
||||
extruder.getProperty("machine_nozzle_offset_x", "value"),
|
||||
extruder.getProperty("machine_nozzle_offset_y", "value")]
|
||||
return result
|
||||
|
||||
def processGCodeFile(self, file_name):
|
||||
Logger.log("d", "Preparing to load %s" % file_name)
|
||||
self._cancelled = False
|
||||
# We obtain the filament diameter from the selected printer to calculate line widths
|
||||
self._filament_diameter = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value")
|
||||
|
||||
scene_node = SceneNode()
|
||||
# Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
|
||||
# real data to calculate it from.
|
||||
scene_node.getBoundingBox = self._getNullBoundingBox
|
||||
|
||||
gcode_list = []
|
||||
self._is_layers_in_file = False
|
||||
|
||||
Logger.log("d", "Opening file %s" % file_name)
|
||||
|
||||
self._extruder_offsets = self._extruderOffsets() # dict with index the extruder number. can be empty
|
||||
|
||||
with open(file_name, "r") as file:
|
||||
file_lines = 0
|
||||
current_line = 0
|
||||
for line in file:
|
||||
file_lines += 1
|
||||
gcode_list.append(line)
|
||||
if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
|
||||
self._is_layers_in_file = True
|
||||
file.seek(0)
|
||||
|
||||
file_step = max(math.floor(file_lines / 100), 1)
|
||||
|
||||
self._clearValues()
|
||||
|
||||
self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"),
|
||||
lifetime=0,
|
||||
title = catalog.i18nc("@info:title", "G-code Details"))
|
||||
|
||||
self._message.setProgress(0)
|
||||
self._message.show()
|
||||
|
||||
Logger.log("d", "Parsing %s..." % file_name)
|
||||
|
||||
current_position = self._position(0, 0, 0, 0, [0])
|
||||
current_path = []
|
||||
min_layer_number = 0
|
||||
negative_layers = 0
|
||||
previous_layer = 0
|
||||
|
||||
for line in file:
|
||||
if self._cancelled:
|
||||
Logger.log("d", "Parsing %s cancelled" % file_name)
|
||||
return None
|
||||
current_line += 1
|
||||
|
||||
if current_line % file_step == 0:
|
||||
self._message.setProgress(math.floor(current_line / file_lines * 100))
|
||||
Job.yieldThread()
|
||||
if len(line) == 0:
|
||||
continue
|
||||
|
||||
if line.find(self._type_keyword) == 0:
|
||||
type = line[len(self._type_keyword):].strip()
|
||||
if type == "WALL-INNER":
|
||||
self._layer_type = LayerPolygon.InsetXType
|
||||
elif type == "WALL-OUTER":
|
||||
self._layer_type = LayerPolygon.Inset0Type
|
||||
elif type == "SKIN":
|
||||
self._layer_type = LayerPolygon.SkinType
|
||||
elif type == "SKIRT":
|
||||
self._layer_type = LayerPolygon.SkirtType
|
||||
elif type == "SUPPORT":
|
||||
self._layer_type = LayerPolygon.SupportType
|
||||
elif type == "FILL":
|
||||
self._layer_type = LayerPolygon.InfillType
|
||||
else:
|
||||
Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
|
||||
|
||||
# When the layer change is reached, the polygon is computed so we have just one layer per layer per extruder
|
||||
if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
|
||||
try:
|
||||
layer_number = int(line[len(self._layer_keyword):])
|
||||
self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
|
||||
current_path.clear()
|
||||
|
||||
# When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior
|
||||
# as in ProcessSlicedLayersJob
|
||||
if layer_number < min_layer_number:
|
||||
min_layer_number = layer_number
|
||||
if layer_number < 0:
|
||||
layer_number += abs(min_layer_number)
|
||||
negative_layers += 1
|
||||
else:
|
||||
layer_number += negative_layers
|
||||
|
||||
# In case there is a gap in the layer count, empty layers are created
|
||||
for empty_layer in range(previous_layer + 1, layer_number):
|
||||
self._createEmptyLayer(empty_layer)
|
||||
|
||||
self._layer_number = layer_number
|
||||
previous_layer = layer_number
|
||||
except:
|
||||
pass
|
||||
|
||||
# This line is a comment. Ignore it (except for the layer_keyword)
|
||||
if line.startswith(";"):
|
||||
continue
|
||||
|
||||
G = self._getInt(line, "G")
|
||||
if G is not None:
|
||||
# When find a movement, the new posistion is calculated and added to the current_path, but
|
||||
# don't need to create a polygon until the end of the layer
|
||||
current_position = self.processGCode(G, line, current_position, current_path)
|
||||
continue
|
||||
|
||||
# When changing the extruder, the polygon with the stored paths is computed
|
||||
if line.startswith("T"):
|
||||
T = self._getInt(line, "T")
|
||||
if T is not None:
|
||||
self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
|
||||
current_path.clear()
|
||||
|
||||
current_position = self.processTCode(T, line, current_position, current_path)
|
||||
|
||||
if line.startswith("M"):
|
||||
M = self._getInt(line, "M")
|
||||
self.processMCode(M, line, current_position, current_path)
|
||||
|
||||
# "Flush" leftovers. Last layer paths are still stored
|
||||
if len(current_path) > 1:
|
||||
if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
|
||||
self._layer_number += 1
|
||||
current_path.clear()
|
||||
|
||||
material_color_map = numpy.zeros((10, 4), dtype = numpy.float32)
|
||||
material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
|
||||
material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
|
||||
layer_mesh = self._layer_data_builder.build(material_color_map)
|
||||
decorator = LayerDataDecorator.LayerDataDecorator()
|
||||
decorator.setLayerData(layer_mesh)
|
||||
scene_node.addDecorator(decorator)
|
||||
|
||||
gcode_list_decorator = GCodeListDecorator()
|
||||
gcode_list_decorator.setGCodeList(gcode_list)
|
||||
scene_node.addDecorator(gcode_list_decorator)
|
||||
|
||||
Application.getInstance().getController().getScene().gcode_list = gcode_list
|
||||
|
||||
Logger.log("d", "Finished parsing %s" % file_name)
|
||||
self._message.hide()
|
||||
|
||||
if self._layer_number == 0:
|
||||
Logger.log("w", "File %s doesn't contain any valid layers" % file_name)
|
||||
|
||||
settings = Application.getInstance().getGlobalContainerStack()
|
||||
machine_width = settings.getProperty("machine_width", "value")
|
||||
machine_depth = settings.getProperty("machine_depth", "value")
|
||||
|
||||
if not self._center_is_zero:
|
||||
scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
|
||||
|
||||
Logger.log("d", "Loaded %s" % file_name)
|
||||
|
||||
if Preferences.getInstance().getValue("gcodereader/show_caution"):
|
||||
caution_message = Message(catalog.i18nc(
|
||||
"@info:generic",
|
||||
"Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."),
|
||||
lifetime=0,
|
||||
title = catalog.i18nc("@info:title", "G-code Details"))
|
||||
caution_message.show()
|
||||
|
||||
# The "save/print" button's state is bound to the backend state.
|
||||
backend = Application.getInstance().getBackend()
|
||||
backend.backendStateChange.emit(Backend.BackendState.Disabled)
|
||||
|
||||
return scene_node
|
|
@ -1,471 +1,44 @@
|
|||
# Copyright (c) 2017 Aleph Objects, Inc.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Backend import Backend
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.FileHandler.FileReader import FileReader
|
||||
from UM.Mesh.MeshReader import MeshReader
|
||||
from UM.Message import Message
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Preferences import Preferences
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
from cura import LayerDataBuilder
|
||||
from cura import LayerDataDecorator
|
||||
from cura.LayerPolygon import LayerPolygon
|
||||
from cura.GCodeListDecorator import GCodeListDecorator
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
import numpy
|
||||
import math
|
||||
import re
|
||||
from collections import namedtuple
|
||||
|
||||
from . import MarlinFlavorParser, RepRapFlavorParser
|
||||
|
||||
# Class for loading and parsing G-code files
|
||||
class GCodeReader(MeshReader):
|
||||
|
||||
_flavor_default = "Marlin"
|
||||
_flavor_keyword = ";FLAVOR:"
|
||||
_flavor_readers_dict = {"RepRap" : RepRapFlavorParser.RepRapFlavorParser(),
|
||||
"Marlin" : MarlinFlavorParser.MarlinFlavorParser()}
|
||||
|
||||
def __init__(self):
|
||||
super(GCodeReader, self).__init__()
|
||||
self._supported_extensions = [".gcode", ".g"]
|
||||
Application.getInstance().hideMessageSignal.connect(self._onHideMessage)
|
||||
self._cancelled = False
|
||||
self._message = None
|
||||
self._layer_number = 0
|
||||
self._extruder_number = 0
|
||||
self._clearValues()
|
||||
self._scene_node = None
|
||||
# X, Y, Z position, F feedrate and E extruder values are stored
|
||||
self._position = namedtuple('Position', ['x', 'y', 'z', 'f', 'e'])
|
||||
self._is_layers_in_file = False # Does the Gcode have the layers comment?
|
||||
self._extruder_offsets = {} # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
|
||||
self._current_layer_thickness = 0.2 # default
|
||||
self._flavor_reader = None
|
||||
|
||||
Preferences.getInstance().addPreference("gcodereader/show_caution", True)
|
||||
|
||||
def _clearValues(self):
|
||||
self._filament_diameter = 2.85
|
||||
self._extruder_number = 0
|
||||
self._extrusion_length_offset = [0]
|
||||
self._layer_type = LayerPolygon.Inset0Type
|
||||
self._layer_number = 0
|
||||
self._previous_z = 0
|
||||
self._layer_data_builder = LayerDataBuilder.LayerDataBuilder()
|
||||
self._center_is_zero = False
|
||||
self._is_absolute_positioning = True # It can be absolute (G90) or relative (G91)
|
||||
self._is_absolute_extrusion = True # It can become absolute (M82, default) or relative (M83)
|
||||
# PreRead is used to get the correct flavor. If not, Marlin is set by default
|
||||
def preRead(self, file_name, *args, **kwargs):
|
||||
with open(file_name, "r") as file:
|
||||
for line in file:
|
||||
if line[:len(self._flavor_keyword)] == self._flavor_keyword:
|
||||
try:
|
||||
self._flavor_reader = self._flavor_readers_dict[line[len(self._flavor_keyword):].rstrip()]
|
||||
return FileReader.PreReadResult.accepted
|
||||
except:
|
||||
# If there is no entry in the dictionary for this flavor, just skip and select the by-default flavor
|
||||
break
|
||||
|
||||
@staticmethod
|
||||
def _getValue(line, code):
|
||||
n = line.find(code)
|
||||
if n < 0:
|
||||
return None
|
||||
n += len(code)
|
||||
pattern = re.compile("[;\s]")
|
||||
match = pattern.search(line, n)
|
||||
m = match.start() if match is not None else -1
|
||||
try:
|
||||
if m < 0:
|
||||
return line[n:]
|
||||
return line[n:m]
|
||||
except:
|
||||
return None
|
||||
|
||||
def _getInt(self, line, code):
|
||||
value = self._getValue(line, code)
|
||||
try:
|
||||
return int(value)
|
||||
except:
|
||||
return None
|
||||
|
||||
def _getFloat(self, line, code):
|
||||
value = self._getValue(line, code)
|
||||
try:
|
||||
return float(value)
|
||||
except:
|
||||
return None
|
||||
|
||||
def _onHideMessage(self, message):
|
||||
if message == self._message:
|
||||
self._cancelled = True
|
||||
|
||||
@staticmethod
|
||||
def _getNullBoundingBox():
|
||||
return AxisAlignedBox(minimum=Vector(0, 0, 0), maximum=Vector(10, 10, 10))
|
||||
|
||||
def _createPolygon(self, layer_thickness, path, extruder_offsets):
|
||||
countvalid = 0
|
||||
for point in path:
|
||||
if point[5] > 0:
|
||||
countvalid += 1
|
||||
if countvalid >= 2:
|
||||
# we know what to do now, no need to count further
|
||||
continue
|
||||
if countvalid < 2:
|
||||
return False
|
||||
try:
|
||||
self._layer_data_builder.addLayer(self._layer_number)
|
||||
self._layer_data_builder.setLayerHeight(self._layer_number, path[0][2])
|
||||
self._layer_data_builder.setLayerThickness(self._layer_number, layer_thickness)
|
||||
this_layer = self._layer_data_builder.getLayer(self._layer_number)
|
||||
except ValueError:
|
||||
return False
|
||||
count = len(path)
|
||||
line_types = numpy.empty((count - 1, 1), numpy.int32)
|
||||
line_widths = numpy.empty((count - 1, 1), numpy.float32)
|
||||
line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
|
||||
line_feedrates = numpy.empty((count - 1, 1), numpy.float32)
|
||||
line_widths[:, 0] = 0.35 # Just a guess
|
||||
line_thicknesses[:, 0] = layer_thickness
|
||||
points = numpy.empty((count, 3), numpy.float32)
|
||||
extrusion_values = numpy.empty((count, 1), numpy.float32)
|
||||
i = 0
|
||||
for point in path:
|
||||
points[i, :] = [point[0] + extruder_offsets[0], point[2], -point[1] - extruder_offsets[1]]
|
||||
extrusion_values[i] = point[4]
|
||||
if i > 0:
|
||||
line_feedrates[i - 1] = point[3]
|
||||
line_types[i - 1] = point[5]
|
||||
if point[5] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]:
|
||||
line_widths[i - 1] = 0.1
|
||||
line_thicknesses[i - 1] = 0.0 # Travels are set as zero thickness lines
|
||||
else:
|
||||
line_widths[i - 1] = self._calculateLineWidth(points[i], points[i-1], extrusion_values[i], extrusion_values[i-1], layer_thickness)
|
||||
i += 1
|
||||
|
||||
this_poly = LayerPolygon(self._extruder_number, line_types, points, line_widths, line_thicknesses, line_feedrates)
|
||||
this_poly.buildCache()
|
||||
|
||||
this_layer.polygons.append(this_poly)
|
||||
return True
|
||||
|
||||
def _createEmptyLayer(self, layer_number):
|
||||
self._layer_data_builder.addLayer(layer_number)
|
||||
self._layer_data_builder.setLayerHeight(layer_number, 0)
|
||||
self._layer_data_builder.setLayerThickness(layer_number, 0)
|
||||
|
||||
def _calculateLineWidth(self, current_point, previous_point, current_extrusion, previous_extrusion, layer_thickness):
|
||||
# Area of the filament
|
||||
Af = (self._filament_diameter / 2) ** 2 * numpy.pi
|
||||
# Length of the extruded filament
|
||||
de = current_extrusion - previous_extrusion
|
||||
# Volumne of the extruded filament
|
||||
dVe = de * Af
|
||||
# Length of the printed line
|
||||
dX = numpy.sqrt((current_point[0] - previous_point[0])**2 + (current_point[2] - previous_point[2])**2)
|
||||
# When the extruder recovers from a retraction, we get zero distance
|
||||
if dX == 0:
|
||||
return 0.1
|
||||
# Area of the printed line. This area is a rectangle
|
||||
Ae = dVe / dX
|
||||
# This area is a rectangle with area equal to layer_thickness * layer_width
|
||||
line_width = Ae / layer_thickness
|
||||
|
||||
# A threshold is set to avoid weird paths in the GCode
|
||||
if line_width > 1.2:
|
||||
return 0.35
|
||||
return line_width
|
||||
|
||||
def _gCode0(self, position, params, path):
|
||||
x, y, z, f, e = position
|
||||
|
||||
if self._is_absolute_positioning:
|
||||
x = params.x if params.x is not None else x
|
||||
y = params.y if params.y is not None else y
|
||||
z = params.z if params.z is not None else z
|
||||
else:
|
||||
x += params.x if params.x is not None else 0
|
||||
y += params.y if params.y is not None else 0
|
||||
z += params.z if params.z is not None else 0
|
||||
|
||||
f = params.f if params.f is not None else f
|
||||
|
||||
if params.e is not None:
|
||||
new_extrusion_value = params.e if self._is_absolute_extrusion else e[self._extruder_number] + params.e
|
||||
if new_extrusion_value > e[self._extruder_number]:
|
||||
path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], self._layer_type]) # extrusion
|
||||
else:
|
||||
path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) # retraction
|
||||
e[self._extruder_number] = new_extrusion_value
|
||||
|
||||
# Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
|
||||
# Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
|
||||
if z > self._previous_z and (z - self._previous_z < 1.5):
|
||||
self._current_layer_thickness = z - self._previous_z # allow a tiny overlap
|
||||
self._previous_z = z
|
||||
else:
|
||||
path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType])
|
||||
return self._position(x, y, z, f, e)
|
||||
|
||||
|
||||
# G0 and G1 should be handled exactly the same.
|
||||
_gCode1 = _gCode0
|
||||
|
||||
## Home the head.
|
||||
def _gCode28(self, position, params, path):
|
||||
return self._position(
|
||||
params.x if params.x is not None else position.x,
|
||||
params.y if params.y is not None else position.y,
|
||||
0,
|
||||
position.f,
|
||||
position.e)
|
||||
|
||||
## Set the absolute positioning
|
||||
def _gCode90(self, position, params, path):
|
||||
self._is_absolute_positioning = True
|
||||
return position
|
||||
|
||||
## Set the relative positioning
|
||||
def _gCode91(self, position, params, path):
|
||||
self._is_absolute_positioning = False
|
||||
return position
|
||||
|
||||
## Reset the current position to the values specified.
|
||||
# For example: G92 X10 will set the X to 10 without any physical motion.
|
||||
def _gCode92(self, position, params, path):
|
||||
if params.e is not None:
|
||||
# Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
|
||||
self._extrusion_length_offset[self._extruder_number] += position.e[self._extruder_number] - params.e
|
||||
position.e[self._extruder_number] = params.e
|
||||
return self._position(
|
||||
params.x if params.x is not None else position.x,
|
||||
params.y if params.y is not None else position.y,
|
||||
params.z if params.z is not None else position.z,
|
||||
params.f if params.f is not None else position.f,
|
||||
position.e)
|
||||
|
||||
def _processGCode(self, G, line, position, path):
|
||||
func = getattr(self, "_gCode%s" % G, None)
|
||||
line = line.split(";", 1)[0] # Remove comments (if any)
|
||||
if func is not None:
|
||||
s = line.upper().split(" ")
|
||||
x, y, z, f, e = None, None, None, None, None
|
||||
for item in s[1:]:
|
||||
if len(item) <= 1:
|
||||
continue
|
||||
if item.startswith(";"):
|
||||
continue
|
||||
if item[0] == "X":
|
||||
x = float(item[1:])
|
||||
if item[0] == "Y":
|
||||
y = float(item[1:])
|
||||
if item[0] == "Z":
|
||||
z = float(item[1:])
|
||||
if item[0] == "F":
|
||||
f = float(item[1:]) / 60
|
||||
if item[0] == "E":
|
||||
e = float(item[1:])
|
||||
if self._is_absolute_positioning and ((x is not None and x < 0) or (y is not None and y < 0)):
|
||||
self._center_is_zero = True
|
||||
params = self._position(x, y, z, f, e)
|
||||
return func(position, params, path)
|
||||
return position
|
||||
|
||||
def _processTCode(self, T, line, position, path):
|
||||
self._extruder_number = T
|
||||
if self._extruder_number + 1 > len(position.e):
|
||||
self._extrusion_length_offset.extend([0] * (self._extruder_number - len(position.e) + 1))
|
||||
position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
|
||||
return position
|
||||
|
||||
def _processMCode(self, M):
|
||||
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:"
|
||||
|
||||
## For showing correct x, y offsets for each extruder
|
||||
def _extruderOffsets(self):
|
||||
result = {}
|
||||
for extruder in ExtruderManager.getInstance().getExtruderStacks():
|
||||
result[int(extruder.getMetaData().get("position", "0"))] = [
|
||||
extruder.getProperty("machine_nozzle_offset_x", "value"),
|
||||
extruder.getProperty("machine_nozzle_offset_y", "value")]
|
||||
return result
|
||||
# If no flavor is found in the GCode, then we use the by-default
|
||||
self._flavor_reader = self._flavor_readers_dict[self._flavor_default]
|
||||
return FileReader.PreReadResult.accepted
|
||||
|
||||
def read(self, file_name):
|
||||
Logger.log("d", "Preparing to load %s" % file_name)
|
||||
self._cancelled = False
|
||||
# We obtain the filament diameter from the selected printer to calculate line widths
|
||||
self._filament_diameter = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value")
|
||||
|
||||
scene_node = SceneNode()
|
||||
# Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
|
||||
# real data to calculate it from.
|
||||
scene_node.getBoundingBox = self._getNullBoundingBox
|
||||
|
||||
gcode_list = []
|
||||
self._is_layers_in_file = False
|
||||
|
||||
Logger.log("d", "Opening file %s" % file_name)
|
||||
|
||||
self._extruder_offsets = self._extruderOffsets() # dict with index the extruder number. can be empty
|
||||
|
||||
last_z = 0
|
||||
with open(file_name, "r") as file:
|
||||
file_lines = 0
|
||||
current_line = 0
|
||||
for line in file:
|
||||
file_lines += 1
|
||||
gcode_list.append(line)
|
||||
if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
|
||||
self._is_layers_in_file = True
|
||||
file.seek(0)
|
||||
|
||||
file_step = max(math.floor(file_lines / 100), 1)
|
||||
|
||||
self._clearValues()
|
||||
|
||||
self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"),
|
||||
lifetime=0,
|
||||
title = catalog.i18nc("@info:title", "G-code Details"))
|
||||
|
||||
self._message.setProgress(0)
|
||||
self._message.show()
|
||||
|
||||
Logger.log("d", "Parsing %s..." % file_name)
|
||||
|
||||
current_position = self._position(0, 0, 0, 0, [0])
|
||||
current_path = []
|
||||
min_layer_number = 0
|
||||
negative_layers = 0
|
||||
previous_layer = 0
|
||||
|
||||
for line in file:
|
||||
if self._cancelled:
|
||||
Logger.log("d", "Parsing %s cancelled" % file_name)
|
||||
return None
|
||||
current_line += 1
|
||||
last_z = current_position.z
|
||||
|
||||
if current_line % file_step == 0:
|
||||
self._message.setProgress(math.floor(current_line / file_lines * 100))
|
||||
Job.yieldThread()
|
||||
if len(line) == 0:
|
||||
continue
|
||||
|
||||
if line.find(self._type_keyword) == 0:
|
||||
type = line[len(self._type_keyword):].strip()
|
||||
if type == "WALL-INNER":
|
||||
self._layer_type = LayerPolygon.InsetXType
|
||||
elif type == "WALL-OUTER":
|
||||
self._layer_type = LayerPolygon.Inset0Type
|
||||
elif type == "SKIN":
|
||||
self._layer_type = LayerPolygon.SkinType
|
||||
elif type == "SKIRT":
|
||||
self._layer_type = LayerPolygon.SkirtType
|
||||
elif type == "SUPPORT":
|
||||
self._layer_type = LayerPolygon.SupportType
|
||||
elif type == "FILL":
|
||||
self._layer_type = LayerPolygon.InfillType
|
||||
else:
|
||||
Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
|
||||
|
||||
# When the layer change is reached, the polygon is computed so we have just one layer per layer per extruder
|
||||
if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
|
||||
try:
|
||||
layer_number = int(line[len(self._layer_keyword):])
|
||||
self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
|
||||
current_path.clear()
|
||||
|
||||
# When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior
|
||||
# as in ProcessSlicedLayersJob
|
||||
if layer_number < min_layer_number:
|
||||
min_layer_number = layer_number
|
||||
if layer_number < 0:
|
||||
layer_number += abs(min_layer_number)
|
||||
negative_layers += 1
|
||||
else:
|
||||
layer_number += negative_layers
|
||||
|
||||
# In case there is a gap in the layer count, empty layers are created
|
||||
for empty_layer in range(previous_layer + 1, layer_number):
|
||||
self._createEmptyLayer(empty_layer)
|
||||
|
||||
self._layer_number = layer_number
|
||||
previous_layer = layer_number
|
||||
except:
|
||||
pass
|
||||
|
||||
# This line is a comment. Ignore it (except for the layer_keyword)
|
||||
if line.startswith(";"):
|
||||
continue
|
||||
|
||||
G = self._getInt(line, "G")
|
||||
if G is not None:
|
||||
# When find a movement, the new posistion is calculated and added to the current_path, but
|
||||
# don't need to create a polygon until the end of the layer
|
||||
current_position = self._processGCode(G, line, current_position, current_path)
|
||||
continue
|
||||
|
||||
# When changing the extruder, the polygon with the stored paths is computed
|
||||
if line.startswith("T"):
|
||||
T = self._getInt(line, "T")
|
||||
if T is not None:
|
||||
self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
|
||||
current_path.clear()
|
||||
|
||||
current_position = self._processTCode(T, line, current_position, current_path)
|
||||
|
||||
if line.startswith("M"):
|
||||
M = self._getInt(line, "M")
|
||||
self._processMCode(M)
|
||||
|
||||
# "Flush" leftovers. Last layer paths are still stored
|
||||
if len(current_path) > 1:
|
||||
if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
|
||||
self._layer_number += 1
|
||||
current_path.clear()
|
||||
|
||||
material_color_map = numpy.zeros((10, 4), dtype = numpy.float32)
|
||||
material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
|
||||
material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
|
||||
layer_mesh = self._layer_data_builder.build(material_color_map)
|
||||
decorator = LayerDataDecorator.LayerDataDecorator()
|
||||
decorator.setLayerData(layer_mesh)
|
||||
scene_node.addDecorator(decorator)
|
||||
|
||||
gcode_list_decorator = GCodeListDecorator()
|
||||
gcode_list_decorator.setGCodeList(gcode_list)
|
||||
scene_node.addDecorator(gcode_list_decorator)
|
||||
|
||||
Application.getInstance().getController().getScene().gcode_list = gcode_list
|
||||
|
||||
Logger.log("d", "Finished parsing %s" % file_name)
|
||||
self._message.hide()
|
||||
|
||||
if self._layer_number == 0:
|
||||
Logger.log("w", "File %s doesn't contain any valid layers" % file_name)
|
||||
|
||||
settings = Application.getInstance().getGlobalContainerStack()
|
||||
machine_width = settings.getProperty("machine_width", "value")
|
||||
machine_depth = settings.getProperty("machine_depth", "value")
|
||||
|
||||
if not self._center_is_zero:
|
||||
scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
|
||||
|
||||
Logger.log("d", "Loaded %s" % file_name)
|
||||
|
||||
if Preferences.getInstance().getValue("gcodereader/show_caution"):
|
||||
caution_message = Message(catalog.i18nc(
|
||||
"@info:generic",
|
||||
"Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."),
|
||||
lifetime=0,
|
||||
title = catalog.i18nc("@info:title", "G-code Details"))
|
||||
caution_message.show()
|
||||
|
||||
# The "save/print" button's state is bound to the backend state.
|
||||
backend = Application.getInstance().getBackend()
|
||||
backend.backendStateChange.emit(Backend.BackendState.Disabled)
|
||||
|
||||
return scene_node
|
||||
return self._flavor_reader.processGCodeFile(file_name)
|
||||
|
|
10
plugins/GCodeReader/MarlinFlavorParser.py
Normal file
10
plugins/GCodeReader/MarlinFlavorParser.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from . import FlavorParser
|
||||
|
||||
# This parser is intented for interpret the Marlin/Sprinter Firmware flavor
|
||||
class MarlinFlavorParser(FlavorParser.FlavorParser):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
32
plugins/GCodeReader/RepRapFlavorParser.py
Normal file
32
plugins/GCodeReader/RepRapFlavorParser.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from . import FlavorParser
|
||||
|
||||
# This parser is intented for interpret the RepRap Firmware flavor
|
||||
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
|
||||
|
||||
## Set the absolute positioning
|
||||
# RepRapFlavor code G90 sets position of X, Y, Z, and E to absolute
|
||||
def _gCode90(self, position, params, path):
|
||||
self._is_absolute_positioning = True
|
||||
self._is_absolute_extrusion = True
|
||||
return position
|
||||
|
||||
## Set the relative positioning
|
||||
# RepRapFlavor code G91 sets position of X, Y, Z to relative
|
||||
# For relative E, M83 is used
|
||||
def _gCode91(self, position, params, path):
|
||||
self._is_absolute_positioning = False
|
||||
return position
|
|
@ -43,7 +43,7 @@ UM.Dialog
|
|||
TextField {
|
||||
id: peak_height
|
||||
objectName: "Peak_Height"
|
||||
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: -500; top: 500;}
|
||||
validator: RegExpValidator {regExp: /^-?\d{1,3}([\,|\.]\d*)?$/}
|
||||
width: 180 * screenScaleFactor
|
||||
onTextChanged: { manager.onPeakHeightChanged(text) }
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ UM.Dialog
|
|||
TextField {
|
||||
id: base_height
|
||||
objectName: "Base_Height"
|
||||
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 0; top: 500;}
|
||||
validator: RegExpValidator {regExp: /^\d{1,3}([\,|\.]\d*)?$/}
|
||||
width: 180 * screenScaleFactor
|
||||
onTextChanged: { manager.onBaseHeightChanged(text) }
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ UM.Dialog
|
|||
id: width
|
||||
objectName: "Width"
|
||||
focus: true
|
||||
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 1; top: 500;}
|
||||
validator: RegExpValidator {regExp: /^[1-9]\d{0,2}([\,|\.]\d*)?$/}
|
||||
width: 180 * screenScaleFactor
|
||||
onTextChanged: { manager.onWidthChanged(text) }
|
||||
}
|
||||
|
@ -113,7 +113,7 @@ UM.Dialog
|
|||
id: depth
|
||||
objectName: "Depth"
|
||||
focus: true
|
||||
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 1; top: 500;}
|
||||
validator: RegExpValidator {regExp: /^[1-9]\d{0,2}([\,|\.]\d*)?$/}
|
||||
width: 180 * screenScaleFactor
|
||||
onTextChanged: { manager.onDepthChanged(text) }
|
||||
}
|
||||
|
|
|
@ -4,8 +4,7 @@
|
|||
import os
|
||||
import threading
|
||||
|
||||
from PyQt5.QtCore import Qt, QUrl, pyqtSignal, QObject
|
||||
from PyQt5.QtQml import QQmlComponent, QQmlContext
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, QObject
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from UM.Application import Application
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
|
@ -81,14 +80,9 @@ class ImageReaderUI(QObject):
|
|||
def _createConfigUI(self):
|
||||
if self._ui_view is None:
|
||||
Logger.log("d", "Creating ImageReader config UI")
|
||||
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("ImageReader"), "ConfigUI.qml"))
|
||||
component = QQmlComponent(Application.getInstance()._engine, path)
|
||||
self._ui_context = QQmlContext(Application.getInstance()._engine.rootContext())
|
||||
self._ui_context.setContextProperty("manager", self)
|
||||
self._ui_view = component.create(self._ui_context)
|
||||
|
||||
path = os.path.join(PluginRegistry.getInstance().getPluginPath("ImageReader"), "ConfigUI.qml")
|
||||
self._ui_view = Application.getInstance().createQmlComponent(path, {"manager": self})
|
||||
self._ui_view.setFlags(self._ui_view.flags() & ~Qt.WindowCloseButtonHint & ~Qt.WindowMinimizeButtonHint & ~Qt.WindowMaximizeButtonHint);
|
||||
|
||||
self._disable_size_callbacks = False
|
||||
|
||||
@pyqtSlot()
|
||||
|
@ -107,7 +101,7 @@ class ImageReaderUI(QObject):
|
|||
def onWidthChanged(self, value):
|
||||
if self._ui_view and not self._disable_size_callbacks:
|
||||
if len(value) > 0:
|
||||
self._width = float(value)
|
||||
self._width = float(value.replace(",", "."))
|
||||
else:
|
||||
self._width = 0
|
||||
|
||||
|
@ -120,7 +114,7 @@ class ImageReaderUI(QObject):
|
|||
def onDepthChanged(self, value):
|
||||
if self._ui_view and not self._disable_size_callbacks:
|
||||
if len(value) > 0:
|
||||
self._depth = float(value)
|
||||
self._depth = float(value.replace(",", "."))
|
||||
else:
|
||||
self._depth = 0
|
||||
|
||||
|
@ -132,14 +126,14 @@ class ImageReaderUI(QObject):
|
|||
@pyqtSlot(str)
|
||||
def onBaseHeightChanged(self, value):
|
||||
if (len(value) > 0):
|
||||
self.base_height = float(value)
|
||||
self.base_height = float(value.replace(",", "."))
|
||||
else:
|
||||
self.base_height = 0
|
||||
|
||||
@pyqtSlot(str)
|
||||
def onPeakHeightChanged(self, value):
|
||||
if (len(value) > 0):
|
||||
self.peak_height = float(value)
|
||||
self.peak_height = float(value.replace(",", "."))
|
||||
else:
|
||||
self.peak_height = 0
|
||||
|
||||
|
@ -149,7 +143,4 @@ class ImageReaderUI(QObject):
|
|||
|
||||
@pyqtSlot(int)
|
||||
def onImageColorInvertChanged(self, value):
|
||||
if (value == 1):
|
||||
self.image_color_invert = True
|
||||
else:
|
||||
self.image_color_invert = False
|
||||
self.image_color_invert = (value == 1)
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import configparser # For reading the legacy profile INI files.
|
||||
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.
|
||||
|
@ -80,8 +81,7 @@ class LegacyProfileReader(ProfileReader):
|
|||
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
try:
|
||||
with open(file_name) as f:
|
||||
parser.readfp(f) # Parse the INI file.
|
||||
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
|
||||
|
@ -138,7 +138,25 @@ class LegacyProfileReader(ProfileReader):
|
|||
|
||||
if len(profile.getAllKeys()) == 0:
|
||||
Logger.log("i", "A legacy profile was imported but everything evaluates to the defaults, creating an empty profile.")
|
||||
profile.setDirty(True)
|
||||
profile.addMetaDataEntry("type", "quality_changes")
|
||||
|
||||
|
||||
# We need to downgrade the container to version 1 (in Cura 2.1) so the upgrade system can correctly upgrade
|
||||
# it to the latest version.
|
||||
profile.addMetaDataEntry("type", "profile")
|
||||
# don't know what quality_type it is based on, so use "normal" by default
|
||||
profile.addMetaDataEntry("quality_type", "normal")
|
||||
return profile
|
||||
profile.setDirty(True)
|
||||
|
||||
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"]
|
||||
stream = io.StringIO()
|
||||
parser.write(stream)
|
||||
data = stream.getvalue()
|
||||
profile.deserialize(data)
|
||||
|
||||
return profile
|
||||
|
|
|
@ -188,29 +188,32 @@ class MachineSettingsAction(MachineAction):
|
|||
# In other words: only continue for the UM2 (extended), but not for the UM2+
|
||||
return
|
||||
|
||||
stacks = ExtruderManager.getInstance().getExtruderStacks()
|
||||
has_materials = self._global_container_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"
|
||||
|
||||
material_container = self._global_container_stack.material
|
||||
|
||||
if has_materials:
|
||||
if "has_materials" in self._global_container_stack.getMetaData():
|
||||
self._global_container_stack.setMetaDataEntry("has_materials", True)
|
||||
else:
|
||||
self._global_container_stack.addMetaDataEntry("has_materials", True)
|
||||
|
||||
# Set the material container to a sane default
|
||||
if material_container == self._empty_container:
|
||||
search_criteria = { "type": "material", "definition": "fdmprinter", "id": self._global_container_stack.getMetaDataEntry("preferred_material")}
|
||||
materials = self._container_registry.findInstanceContainers(**search_criteria)
|
||||
if materials:
|
||||
self._global_container_stack.material = materials[0]
|
||||
# Set the material container for each extruder to a sane default
|
||||
for stack in stacks:
|
||||
material_container = stack.material
|
||||
if material_container == self._empty_container:
|
||||
machine_approximate_diameter = str(round(self._global_container_stack.getProperty("material_diameter", "value")))
|
||||
search_criteria = { "type": "material", "definition": "fdmprinter", "id": self._global_container_stack.getMetaDataEntry("preferred_material"), "approximate_diameter": machine_approximate_diameter}
|
||||
materials = self._container_registry.findInstanceContainers(**search_criteria)
|
||||
if materials:
|
||||
stack.material = materials[0]
|
||||
else:
|
||||
# The metadata entry is stored in an ini, and ini files are parsed as strings only.
|
||||
# Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
|
||||
if "has_materials" in self._global_container_stack.getMetaData():
|
||||
self._global_container_stack.removeMetaDataEntry("has_materials")
|
||||
|
||||
self._global_container_stack.material = ContainerRegistry.getInstance().getEmptyInstanceContainer()
|
||||
for stack in stacks:
|
||||
stack.material = ContainerRegistry.getInstance().getEmptyInstanceContainer()
|
||||
|
||||
Application.getInstance().globalContainerStackChanged.emit()
|
||||
|
||||
|
|
|
@ -283,6 +283,7 @@ Cura.MachineAction
|
|||
Loader
|
||||
{
|
||||
id: materialDiameterField
|
||||
visible: Cura.MachineManager.hasMaterials
|
||||
sourceComponent: numericTextFieldWithUnit
|
||||
property string settingKey: "material_diameter"
|
||||
property string unit: catalog.i18nc("@label", "mm")
|
||||
|
@ -355,7 +356,7 @@ Cura.MachineAction
|
|||
if(currentIndex > 0)
|
||||
{
|
||||
contentItem.forceActiveFocus();
|
||||
ExtruderManager.setActiveExtruderIndex(currentIndex - 1);
|
||||
Cura.ExtruderManager.setActiveExtruderIndex(currentIndex - 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -585,11 +586,11 @@ Cura.MachineAction
|
|||
propertyProvider.setPropertyValue("value", text);
|
||||
if(_forceUpdateOnChange)
|
||||
{
|
||||
var extruderIndex = ExtruderManager.activeExtruderIndex;
|
||||
var extruderIndex = Cura.ExtruderManager.activeExtruderIndex;
|
||||
manager.forceUpdate();
|
||||
if(ExtruderManager.activeExtruderIndex != extruderIndex)
|
||||
if(Cura.ExtruderManager.activeExtruderIndex != extruderIndex)
|
||||
{
|
||||
ExtruderManager.setActiveExtruderIndex(extruderIndex)
|
||||
Cura.ExtruderManager.setActiveExtruderIndex(extruderIndex)
|
||||
}
|
||||
}
|
||||
if(_afterOnEditingFinished)
|
||||
|
|
|
@ -26,17 +26,92 @@ Item {
|
|||
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
|
||||
Row
|
||||
{
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@label","Mesh Type")
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
height: UM.Theme.getSize("setting").height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
ComboBox
|
||||
{
|
||||
id: meshTypeSelection
|
||||
style: UM.Theme.styles.combobox
|
||||
onActivated: {
|
||||
UM.ActiveTool.setProperty("MeshType", model.get(index).type)
|
||||
}
|
||||
model: ListModel
|
||||
{
|
||||
id: meshTypeModel
|
||||
Component.onCompleted:
|
||||
{
|
||||
meshTypeModel.append({
|
||||
type: "",
|
||||
text: catalog.i18nc("@label", "Normal model")
|
||||
});
|
||||
meshTypeModel.append({
|
||||
type: "support_mesh",
|
||||
text: catalog.i18nc("@label", "Print as support")
|
||||
});
|
||||
meshTypeModel.append({
|
||||
type: "anti_overhang_mesh",
|
||||
text: catalog.i18nc("@label", "Don't support overlap with other models")
|
||||
});
|
||||
meshTypeModel.append({
|
||||
type: "cutting_mesh",
|
||||
text: catalog.i18nc("@label", "Modify settings for overlap with other models")
|
||||
});
|
||||
meshTypeModel.append({
|
||||
type: "infill_mesh",
|
||||
text: catalog.i18nc("@label", "Modify settings for infill of other models")
|
||||
});
|
||||
|
||||
meshTypeSelection.updateCurrentIndex();
|
||||
}
|
||||
}
|
||||
|
||||
function updateCurrentIndex()
|
||||
{
|
||||
var mesh_type = UM.ActiveTool.properties.getValue("MeshType");
|
||||
for(var index=0; index < meshTypeSelection.model.count; index++)
|
||||
{
|
||||
if(meshTypeSelection.model.get(index).type == mesh_type)
|
||||
{
|
||||
meshTypeSelection.currentIndex = index;
|
||||
return;
|
||||
}
|
||||
}
|
||||
meshTypeSelection.currentIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Connections
|
||||
{
|
||||
target: UM.Selection
|
||||
onSelectionChanged: meshTypeSelection.updateCurrentIndex()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Column
|
||||
{
|
||||
// This is to ensure that the panel is first increasing in size up to 200 and then shows a scrollbar.
|
||||
// It kinda looks ugly otherwise (big panel, no content on it)
|
||||
id: currentSettings
|
||||
property int maximumHeight: 200 * screenScaleFactor
|
||||
height: Math.min(contents.count * (UM.Theme.getSize("section").height + UM.Theme.getSize("default_lining").height), maximumHeight)
|
||||
visible: ["support_mesh", "anti_overhang_mesh"].indexOf(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type) == -1
|
||||
|
||||
ScrollView
|
||||
{
|
||||
height: parent.height
|
||||
width: UM.Theme.getSize("setting").width + UM.Theme.getSize("setting").height
|
||||
width: UM.Theme.getSize("setting").width
|
||||
style: UM.Theme.styles.scrollview
|
||||
|
||||
ListView
|
||||
|
@ -49,6 +124,7 @@ Item {
|
|||
id: addedSettingsModel;
|
||||
containerId: Cura.MachineManager.activeDefinitionId
|
||||
expanded: [ "*" ]
|
||||
exclude: [ "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ]
|
||||
|
||||
visibilityHandler: Cura.PerObjectSettingVisibilityHandler
|
||||
{
|
||||
|
@ -58,6 +134,7 @@ Item {
|
|||
|
||||
delegate: Row
|
||||
{
|
||||
spacing: - UM.Theme.getSize("default_margin").width
|
||||
Loader
|
||||
{
|
||||
id: settingLoader
|
||||
|
@ -112,7 +189,7 @@ Item {
|
|||
|
||||
Button
|
||||
{
|
||||
width: (UM.Theme.getSize("setting").height / 2) | 0
|
||||
width: Math.floor(UM.Theme.getSize("setting").height / 2)
|
||||
height: UM.Theme.getSize("setting").height
|
||||
|
||||
onClicked: addedSettingsModel.setVisible(model.key, false)
|
||||
|
@ -125,7 +202,7 @@ Item {
|
|||
{
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width
|
||||
height: parent.height / 2
|
||||
height: width
|
||||
sourceSize.width: width
|
||||
sourceSize.height: width
|
||||
color: control.hovered ? UM.Theme.getColor("setting_control_button_hover") : UM.Theme.getColor("setting_control_button")
|
||||
|
@ -201,9 +278,9 @@ Item {
|
|||
|
||||
Button
|
||||
{
|
||||
id: customise_settings_button;
|
||||
height: UM.Theme.getSize("setting").height;
|
||||
visible: parseInt(UM.Preferences.getValue("cura/active_mode")) == 1
|
||||
id: customiseSettingsButton;
|
||||
height: UM.Theme.getSize("setting_control").height;
|
||||
visible: currentSettings.visible
|
||||
|
||||
text: catalog.i18nc("@action:button", "Select settings");
|
||||
|
||||
|
@ -223,21 +300,12 @@ Item {
|
|||
{
|
||||
text: control.text;
|
||||
color: UM.Theme.getColor("setting_control_text");
|
||||
font: UM.Theme.getFont("default")
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: settingPickDialog.visible = true;
|
||||
|
||||
Connections
|
||||
{
|
||||
target: UM.Preferences;
|
||||
|
||||
onPreferenceChanged:
|
||||
{
|
||||
customise_settings_button.visible = parseInt(UM.Preferences.getValue("cura/active_mode"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -325,7 +393,7 @@ Item {
|
|||
}
|
||||
visibilityHandler: UM.SettingPreferenceVisibilityHandler {}
|
||||
expanded: [ "*" ]
|
||||
exclude: [ "machine_settings", "command_line_settings" ]
|
||||
exclude: [ "machine_settings", "command_line_settings", "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ]
|
||||
}
|
||||
delegate:Loader
|
||||
{
|
||||
|
|
|
@ -8,6 +8,7 @@ from UM.Application import Application
|
|||
from UM.Preferences import Preferences
|
||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from UM.Settings.SettingInstance import SettingInstance
|
||||
from UM.Event import Event
|
||||
|
||||
|
||||
|
@ -18,7 +19,7 @@ class PerObjectSettingsTool(Tool):
|
|||
super().__init__()
|
||||
self._model = None
|
||||
|
||||
self.setExposedProperties("SelectedObjectId", "ContainerID", "SelectedActiveExtruder")
|
||||
self.setExposedProperties("SelectedObjectId", "ContainerID", "SelectedActiveExtruder", "MeshType")
|
||||
|
||||
self._advanced_mode = False
|
||||
self._multi_extrusion = False
|
||||
|
@ -70,6 +71,39 @@ class PerObjectSettingsTool(Tool):
|
|||
selected_object.addDecorator(SettingOverrideDecorator())
|
||||
selected_object.callDecoration("setActiveExtruder", extruder_stack_id)
|
||||
|
||||
def setMeshType(self, mesh_type):
|
||||
selected_object = Selection.getSelectedObject(0)
|
||||
stack = selected_object.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
|
||||
if not stack:
|
||||
selected_object.addDecorator(SettingOverrideDecorator())
|
||||
stack = selected_object.callDecoration("getStack")
|
||||
|
||||
settings = stack.getTop()
|
||||
for property_key in ["infill_mesh", "cutting_mesh", "support_mesh", "anti_overhang_mesh"]:
|
||||
if property_key != mesh_type:
|
||||
if settings.getInstance(property_key):
|
||||
settings.removeInstance(property_key)
|
||||
else:
|
||||
if not (settings.getInstance(property_key) and settings.getProperty(property_key, "value")):
|
||||
definition = stack.getSettingDefinition(property_key)
|
||||
new_instance = SettingInstance(definition, settings)
|
||||
new_instance.setProperty("value", True)
|
||||
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
||||
settings.addInstance(new_instance)
|
||||
|
||||
def getMeshType(self):
|
||||
selected_object = Selection.getSelectedObject(0)
|
||||
stack = selected_object.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
|
||||
if not stack:
|
||||
return ""
|
||||
|
||||
settings = stack.getTop()
|
||||
for property_key in ["infill_mesh", "cutting_mesh", "support_mesh", "anti_overhang_mesh"]:
|
||||
if settings.getInstance(property_key) and settings.getProperty(property_key, "value"):
|
||||
return property_key
|
||||
|
||||
return ""
|
||||
|
||||
def _onPreferenceChanged(self, preference):
|
||||
if preference == "cura/active_mode":
|
||||
self._advanced_mode = Preferences.getInstance().getValue(preference) == 1
|
||||
|
|
|
@ -11,7 +11,6 @@ from UM.Message import Message
|
|||
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
||||
from PyQt5.QtCore import QUrl, QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtQml import QQmlComponent, QQmlContext
|
||||
|
||||
import json
|
||||
import os
|
||||
|
@ -39,8 +38,6 @@ class PluginBrowser(QObject, Extension):
|
|||
self._plugins_metadata = []
|
||||
self._plugins_model = None
|
||||
|
||||
self._qml_component = None
|
||||
self._qml_context = None
|
||||
self._dialog = None
|
||||
self._download_progress = 0
|
||||
|
||||
|
@ -111,17 +108,8 @@ class PluginBrowser(QObject, Extension):
|
|||
|
||||
def _createDialog(self, qml_name):
|
||||
Logger.log("d", "Creating dialog [%s]", qml_name)
|
||||
|
||||
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), qml_name))
|
||||
self._qml_component = QQmlComponent(Application.getInstance()._engine, path)
|
||||
|
||||
# We need access to engine (although technically we can't)
|
||||
self._qml_context = QQmlContext(Application.getInstance()._engine.rootContext())
|
||||
self._qml_context.setContextProperty("manager", self)
|
||||
dialog = self._qml_component.create(self._qml_context)
|
||||
if dialog is None:
|
||||
Logger.log("e", "QQmlComponent status %s", self._qml_component.status())
|
||||
Logger.log("e", "QQmlComponent errorString %s", self._qml_component.errorString())
|
||||
path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), qml_name)
|
||||
dialog = Application.getInstance().createQmlComponent(path, {"manager": self})
|
||||
return dialog
|
||||
|
||||
def setIsDownloading(self, is_downloading):
|
||||
|
|
|
@ -154,6 +154,7 @@ class SimulationView(View):
|
|||
def _onSceneChanged(self, node):
|
||||
self.setActivity(False)
|
||||
self.calculateMaxLayers()
|
||||
self.calculateMaxPathsOnLayer(self._current_layer_num)
|
||||
|
||||
def isBusy(self):
|
||||
return self._busy
|
||||
|
|
|
@ -97,6 +97,8 @@ Item
|
|||
// if we are in compatibility mode, we only show the "line type"
|
||||
property bool show_legend: UM.SimulationView.compatibilityMode ? true : UM.Preferences.getValue("layerview/layer_view_type") == 1
|
||||
property bool show_gradient: UM.SimulationView.compatibilityMode ? false : UM.Preferences.getValue("layerview/layer_view_type") == 2 || UM.Preferences.getValue("layerview/layer_view_type") == 3
|
||||
property bool show_feedrate_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 2
|
||||
property bool show_thickness_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 3
|
||||
property bool only_show_top_layers: UM.Preferences.getValue("view/only_show_top_layers")
|
||||
property int top_layer_count: UM.Preferences.getValue("view/top_layer_count")
|
||||
|
||||
|
@ -170,6 +172,9 @@ Item
|
|||
{
|
||||
// update visibility of legends
|
||||
viewSettings.show_legend = UM.SimulationView.compatibilityMode || (type_id == 1);
|
||||
viewSettings.show_gradient = !UM.SimulationView.compatibilityMode && (type_id == 2 || type_id == 3);
|
||||
viewSettings.show_feedrate_gradient = viewSettings.show_gradient && (type_id == 2);
|
||||
viewSettings.show_thickness_gradient = viewSettings.show_gradient && (type_id == 3);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -450,11 +455,42 @@ Item
|
|||
}
|
||||
}
|
||||
|
||||
// Gradient colors for feedrate and thickness
|
||||
// Gradient colors for feedrate
|
||||
Rectangle { // In QML 5.9 can be changed by LinearGradient
|
||||
// Invert values because then the bar is rotated 90 degrees
|
||||
id: gradient
|
||||
visible: viewSettings.show_gradient
|
||||
id: feedrateGradient
|
||||
visible: viewSettings.show_feedrate_gradient
|
||||
anchors.left: parent.right
|
||||
height: parent.width
|
||||
width: UM.Theme.getSize("layerview_row").height * 1.5
|
||||
border.width: UM.Theme.getSize("default_lining").width
|
||||
border.color: UM.Theme.getColor("lining")
|
||||
transform: Rotation {origin.x: 0; origin.y: 0; angle: 90}
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0.000
|
||||
color: Qt.rgba(1, 0.5, 0, 1)
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.625
|
||||
color: Qt.rgba(0.375, 0.5, 0, 1)
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.75
|
||||
color: Qt.rgba(0.25, 1, 0, 1)
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: Qt.rgba(0, 0, 1, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gradient colors for layer thickness
|
||||
Rectangle { // In QML 5.9 can be changed by LinearGradient
|
||||
// Invert values because then the bar is rotated 90 degrees
|
||||
id: thicknessGradient
|
||||
visible: viewSettings.show_thickness_gradient
|
||||
anchors.left: parent.right
|
||||
height: parent.width
|
||||
width: UM.Theme.getSize("layerview_row").height * 1.5
|
||||
|
@ -468,15 +504,15 @@ Item
|
|||
}
|
||||
GradientStop {
|
||||
position: 0.25
|
||||
color: Qt.rgba(0.75, 0.5, 0.25, 1)
|
||||
color: Qt.rgba(0.5, 0.5, 0, 1)
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.5
|
||||
color: Qt.rgba(0.5, 1, 0.5, 1)
|
||||
color: Qt.rgba(0, 1, 0, 1)
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.75
|
||||
color: Qt.rgba(0.25, 0.5, 0.75, 1)
|
||||
color: Qt.rgba(0, 0.5, 0.5, 1)
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
|
@ -503,9 +539,9 @@ Item
|
|||
id: pathSlider
|
||||
|
||||
height: UM.Theme.getSize("slider_handle").width
|
||||
anchors.right: playButton.left
|
||||
anchors.rightMargin: UM.Theme.getSize("default_margin").width
|
||||
anchors.left: parent.left
|
||||
anchors.left: playButton.right
|
||||
anchors.leftMargin: UM.Theme.getSize("default_margin").width
|
||||
anchors.right: parent.right
|
||||
visible: !UM.SimulationView.compatibilityMode
|
||||
|
||||
// custom properties
|
||||
|
@ -539,7 +575,7 @@ Item
|
|||
height: UM.Theme.getSize("layerview_menu_size").height
|
||||
|
||||
anchors {
|
||||
top: !UM.SimulationView.compatibilityMode ? playButton.bottom : parent.top
|
||||
top: !UM.SimulationView.compatibilityMode ? pathSlider.bottom : parent.top
|
||||
topMargin: !UM.SimulationView.compatibilityMode ? UM.Theme.getSize("default_margin").height : 0
|
||||
right: parent.right
|
||||
rightMargin: UM.Theme.getSize("slider_layerview_margin").width
|
||||
|
@ -577,13 +613,10 @@ Item
|
|||
// Play simulation button
|
||||
Button {
|
||||
id: playButton
|
||||
implicitWidth: Math.floor(UM.Theme.getSize("button").width * 0.75)
|
||||
implicitHeight: Math.floor(UM.Theme.getSize("button").height * 0.75)
|
||||
iconSource: "./resources/simulation_resume.svg"
|
||||
style: UM.Theme.styles.tool_button
|
||||
style: UM.Theme.styles.small_tool_button
|
||||
visible: !UM.SimulationView.compatibilityMode
|
||||
anchors {
|
||||
horizontalCenter: layerSlider.horizontalCenter
|
||||
verticalCenter: pathSlider.verticalCenter
|
||||
}
|
||||
|
||||
|
|
|
@ -38,12 +38,25 @@ vertex41core =
|
|||
out highp vec3 f_vertex;
|
||||
out highp vec3 f_normal;
|
||||
|
||||
vec4 gradientColor(float abs_value, float min_value, float max_value)
|
||||
vec4 feedrateGradientColor(float abs_value, float min_value, float max_value)
|
||||
{
|
||||
float value = (abs_value - min_value)/(max_value - min_value);
|
||||
float red = value;
|
||||
float green = 1-abs(1-4*value);
|
||||
if (value > 0.375)
|
||||
{
|
||||
green = 0.5;
|
||||
}
|
||||
float blue = max(1-4*value, 0);
|
||||
return vec4(red, green, blue, 1.0);
|
||||
}
|
||||
|
||||
vec4 layerThicknessGradientColor(float abs_value, float min_value, float max_value)
|
||||
{
|
||||
float value = (abs_value - min_value)/(max_value - min_value);
|
||||
float red = max(2*value-1, 0);
|
||||
float green = 1-abs(1-2*value);
|
||||
float blue = 1-value;
|
||||
float blue = max(1-2*value, 0);
|
||||
return vec4(red, green, blue, 1.0);
|
||||
}
|
||||
|
||||
|
@ -64,10 +77,10 @@ vertex41core =
|
|||
v_color = a_color;
|
||||
break;
|
||||
case 2: // "Feedrate"
|
||||
v_color = gradientColor(a_feedrate, u_min_feedrate, u_max_feedrate);
|
||||
v_color = feedrateGradientColor(a_feedrate, u_min_feedrate, u_max_feedrate);
|
||||
break;
|
||||
case 3: // "Layer thickness"
|
||||
v_color = gradientColor(a_line_dim.y, u_min_thickness, u_max_thickness);
|
||||
v_color = layerThicknessGradientColor(a_line_dim.y, u_min_thickness, u_max_thickness);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
|
@ -7,73 +9,71 @@
|
|||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
viewBox="0 0 30 30"
|
||||
width="6mm"
|
||||
height="6mm"
|
||||
viewBox="0 0 5.9999999 6"
|
||||
version="1.1"
|
||||
id="svg4620"
|
||||
sodipodi:docname="simulation_pause.svg"
|
||||
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)">
|
||||
id="svg877"
|
||||
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
|
||||
sodipodi:docname="simulation_pause2.svg">
|
||||
<defs
|
||||
id="defs871" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="15.839192"
|
||||
inkscape:cx="-5.3551409"
|
||||
inkscape:cy="17.386031"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="2880"
|
||||
inkscape:window-height="1675"
|
||||
inkscape:window-x="-13"
|
||||
inkscape:window-y="-13"
|
||||
inkscape:window-maximized="1"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<metadata
|
||||
id="metadata4626">
|
||||
id="metadata874">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs4624" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1137"
|
||||
id="namedview4622"
|
||||
showgrid="false"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:zoom="22.250293"
|
||||
inkscape:cx="8.1879003"
|
||||
inkscape:cy="12.643765"
|
||||
inkscape:window-x="2872"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4620">
|
||||
<sodipodi:guide
|
||||
position="15,15"
|
||||
orientation="1,0"
|
||||
id="guide4628"
|
||||
inkscape:locked="false"
|
||||
inkscape:label=""
|
||||
inkscape:color="rgb(0,0,255)" />
|
||||
<sodipodi:guide
|
||||
position="15,15"
|
||||
orientation="0,1"
|
||||
id="guide4630"
|
||||
inkscape:locked="false"
|
||||
inkscape:label=""
|
||||
inkscape:color="rgb(0,0,255)" />
|
||||
</sodipodi:namedview>
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect5192"
|
||||
width="2"
|
||||
height="20"
|
||||
x="19"
|
||||
y="5" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect5192-5"
|
||||
width="2"
|
||||
height="20"
|
||||
x="9"
|
||||
y="5" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-11.163774,-122.8006)">
|
||||
<g
|
||||
id="g825"
|
||||
transform="matrix(0.26458333,0,0,0.26458333,10.185689,121.85192)">
|
||||
<rect
|
||||
y="5"
|
||||
x="19"
|
||||
height="20"
|
||||
width="2.7552757"
|
||||
id="rect5192"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.34745646;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<rect
|
||||
y="5"
|
||||
x="9"
|
||||
height="20"
|
||||
width="2.7552757"
|
||||
id="rect5192-5"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.34745646;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.4 KiB |
|
@ -1,4 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
|
@ -7,76 +9,70 @@
|
|||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
viewBox="0 0 30 30"
|
||||
width="6mm"
|
||||
height="6mm"
|
||||
viewBox="0 0 6 6"
|
||||
version="1.1"
|
||||
id="svg3765"
|
||||
sodipodi:docname="simulation_resume.svg"
|
||||
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)">
|
||||
id="svg8"
|
||||
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
|
||||
sodipodi:docname="simulation_resume2.svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="15.839192"
|
||||
inkscape:cx="-32.404712"
|
||||
inkscape:cy="14.267522"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="2880"
|
||||
inkscape:window-height="1675"
|
||||
inkscape:window-x="-13"
|
||||
inkscape:window-y="-13"
|
||||
inkscape:window-maximized="1"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<metadata
|
||||
id="metadata3771">
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs3769" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1137"
|
||||
id="namedview3767"
|
||||
showgrid="false"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:zoom="23.327047"
|
||||
inkscape:cx="10.788646"
|
||||
inkscape:cy="14.67951"
|
||||
inkscape:window-x="2872"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg3765">
|
||||
<sodipodi:guide
|
||||
position="15,15"
|
||||
orientation="0,1"
|
||||
id="guide4592"
|
||||
inkscape:locked="false"
|
||||
inkscape:label=""
|
||||
inkscape:color="rgb(0,0,255)" />
|
||||
<sodipodi:guide
|
||||
position="15,15"
|
||||
orientation="1,0"
|
||||
id="guide4594"
|
||||
inkscape:locked="false"
|
||||
inkscape:label=""
|
||||
inkscape:color="rgb(0,0,255)" />
|
||||
</sodipodi:namedview>
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
id="path3783"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="12.732001"
|
||||
sodipodi:cy="14.695877"
|
||||
sodipodi:r1="13.891838"
|
||||
sodipodi:r2="6.945919"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:flatsided="true"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 26.623839,14.695877 -20.8377567,12.030685 0,-24.0613696 z"
|
||||
inkscape:transform-center-x="-2.9211205"
|
||||
style="fill:none;stroke:#000000;stroke-width:2.32790732;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
transform="matrix(0.84110413,0,0,0.87756418,1.775541,2.1034247)" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(81.024887,-389.647)">
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.50520164;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path847"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="-78.732257"
|
||||
sodipodi:cy="392.65222"
|
||||
sodipodi:r1="3.0592039"
|
||||
sodipodi:r2="1.5296021"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:flatsided="true"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m -75.67305,392.65222 -4.588806,2.64935 v -5.2987 z"
|
||||
inkscape:transform-center-x="0.75529536"
|
||||
inkscape:transform-center-y="0.40090429" />
|
||||
</g>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.4 KiB |
|
@ -39,7 +39,7 @@ class SliceInfo(Extension):
|
|||
Preferences.getInstance().addPreference("info/send_slice_info", True)
|
||||
Preferences.getInstance().addPreference("info/asked_send_slice_info", False)
|
||||
|
||||
if not Preferences.getInstance().getValue("info/asked_send_slice_info"):
|
||||
if not Preferences.getInstance().getValue("info/asked_send_slice_info") and Preferences.getInstance().getValue("info/send_slice_info"):
|
||||
self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura collects anonymised slicing statistics. You can disable this in the preferences."),
|
||||
lifetime = 0,
|
||||
dismissable = False,
|
||||
|
|
|
@ -27,31 +27,39 @@ class SolidView(View):
|
|||
|
||||
self._enabled_shader = None
|
||||
self._disabled_shader = None
|
||||
self._non_printing_shader = None
|
||||
|
||||
self._extruders_model = ExtrudersModel()
|
||||
self._theme = None
|
||||
|
||||
def beginRendering(self):
|
||||
scene = self.getController().getScene()
|
||||
renderer = self.getRenderer()
|
||||
|
||||
if not self._theme:
|
||||
self._theme = Application.getInstance().getTheme()
|
||||
|
||||
if not self._enabled_shader:
|
||||
self._enabled_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "overhang.shader"))
|
||||
theme = Application.getInstance().getTheme()
|
||||
self._enabled_shader.setUniformValue("u_overhangColor", Color(*theme.getColor("model_overhang").getRgb()))
|
||||
self._enabled_shader.setUniformValue("u_overhangColor", Color(*self._theme.getColor("model_overhang").getRgb()))
|
||||
|
||||
if not self._disabled_shader:
|
||||
self._disabled_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "striped.shader"))
|
||||
theme = Application.getInstance().getTheme()
|
||||
self._disabled_shader.setUniformValue("u_diffuseColor1", Color(*theme.getColor("model_unslicable").getRgb()))
|
||||
self._disabled_shader.setUniformValue("u_diffuseColor2", Color(*theme.getColor("model_unslicable_alt").getRgb()))
|
||||
self._disabled_shader.setUniformValue("u_diffuseColor1", Color(*self._theme.getColor("model_unslicable").getRgb()))
|
||||
self._disabled_shader.setUniformValue("u_diffuseColor2", Color(*self._theme.getColor("model_unslicable_alt").getRgb()))
|
||||
self._disabled_shader.setUniformValue("u_width", 50.0)
|
||||
|
||||
if not self._non_printing_shader:
|
||||
self._non_printing_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "transparent_object.shader"))
|
||||
self._non_printing_shader.setUniformValue("u_diffuseColor", Color(*self._theme.getColor("model_non_printing").getRgb()))
|
||||
self._non_printing_shader.setUniformValue("u_opacity", 0.6)
|
||||
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
support_extruder_nr = global_container_stack.getProperty("support_extruder_nr", "value")
|
||||
support_angle_stack = ExtruderManager.getInstance().getExtruderStack(support_extruder_nr)
|
||||
support_angle_stack = Application.getInstance().getExtruderManager().getExtruderStack(support_extruder_nr)
|
||||
|
||||
if Preferences.getInstance().getValue("view/show_overhang"):
|
||||
if support_angle_stack is not None and Preferences.getInstance().getValue("view/show_overhang"):
|
||||
angle = support_angle_stack.getProperty("support_angle", "value")
|
||||
# Make sure the overhang angle is valid before passing it to the shader
|
||||
# Note: if the overhang angle is set to its default value, it does not need to get validated (validationState = None)
|
||||
|
@ -68,11 +76,19 @@ class SolidView(View):
|
|||
uniforms = {}
|
||||
shade_factor = 1.0
|
||||
|
||||
per_mesh_stack = node.callDecoration("getStack")
|
||||
|
||||
# Get color to render this mesh in from ExtrudersModel
|
||||
extruder_index = 0
|
||||
extruder_id = node.callDecoration("getActiveExtruder")
|
||||
if extruder_id:
|
||||
extruder_index = max(0, self._extruders_model.find("id", extruder_id))
|
||||
|
||||
# Use the support extruder instead of the active extruder if this is a support_mesh
|
||||
if per_mesh_stack:
|
||||
if per_mesh_stack.getProperty("support_mesh", "value"):
|
||||
extruder_index = int(global_container_stack.getProperty("support_extruder_nr", "value"))
|
||||
|
||||
try:
|
||||
material_color = self._extruders_model.getItem(extruder_index)["color"]
|
||||
except KeyError:
|
||||
|
@ -94,19 +110,17 @@ class SolidView(View):
|
|||
except ValueError:
|
||||
pass
|
||||
|
||||
if hasattr(node, "_outside_buildarea"):
|
||||
if node._outside_buildarea:
|
||||
renderer.queueNode(node, shader = self._disabled_shader)
|
||||
if getattr(node, "_non_printing_mesh", False):
|
||||
if per_mesh_stack and (per_mesh_stack.getProperty("infill_mesh", "value") or per_mesh_stack.getProperty("cutting_mesh", "value")):
|
||||
renderer.queueNode(node, shader = self._non_printing_shader, uniforms = uniforms, transparent = True)
|
||||
else:
|
||||
renderer.queueNode(node, shader = self._enabled_shader, uniforms = uniforms)
|
||||
renderer.queueNode(node, shader = self._non_printing_shader, transparent = True)
|
||||
elif getattr(node, "_outside_buildarea", False):
|
||||
renderer.queueNode(node, shader = self._disabled_shader)
|
||||
else:
|
||||
renderer.queueNode(node, material = self._enabled_shader, uniforms = uniforms)
|
||||
renderer.queueNode(node, shader = self._enabled_shader, uniforms = uniforms)
|
||||
if node.callDecoration("isGroup") and Selection.isSelected(node):
|
||||
renderer.queueNode(scene.getRoot(), mesh = node.getBoundingBoxMesh(), mode = RenderBatch.RenderMode.LineLoop)
|
||||
|
||||
def endRendering(self):
|
||||
pass
|
||||
|
||||
#def _onPreferenceChanged(self, preference):
|
||||
#if preference == "view/show_overhang": ## Todo: This a printer only setting. Should be removed from Uranium.
|
||||
#self._enabled_material = None
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
from cura.MachineAction import MachineAction
|
||||
import os.path
|
||||
import time
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot, QObject
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Logger import Logger
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot, QUrl, QObject
|
||||
from PyQt5.QtQml import QQmlComponent, QQmlContext
|
||||
|
||||
import os.path
|
||||
|
||||
import time
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
|
||||
from cura.MachineAction import MachineAction
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
class DiscoverUM3Action(MachineAction):
|
||||
|
@ -139,17 +137,14 @@ class DiscoverUM3Action(MachineAction):
|
|||
|
||||
def _createAdditionalComponentsView(self):
|
||||
Logger.log("d", "Creating additional ui components for UM3.")
|
||||
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), "UM3InfoComponents.qml"))
|
||||
self.__additional_component = QQmlComponent(Application.getInstance()._engine, path)
|
||||
|
||||
# We need access to engine (although technically we can't)
|
||||
self.__additional_components_context = QQmlContext(Application.getInstance()._engine.rootContext())
|
||||
self.__additional_components_context.setContextProperty("manager", self)
|
||||
|
||||
self.__additional_components_view = self.__additional_component.create(self.__additional_components_context)
|
||||
# Create networking dialog
|
||||
path = os.path.join(PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), "UM3InfoComponents.qml")
|
||||
self.__additional_components_view = Application.getInstance().createQmlComponent(path, {"manager": self})
|
||||
if not self.__additional_components_view:
|
||||
Logger.log("w", "Could not create ui components for UM3.")
|
||||
return
|
||||
|
||||
# Create extra components
|
||||
Application.getInstance().addAdditionalComponent("monitorButtons", self.__additional_components_view.findChild(QObject, "networkPrinterConnectButton"))
|
||||
Application.getInstance().addAdditionalComponent("machinesDetailPane", self.__additional_components_view.findChild(QObject, "networkPrinterConnectionInfo"))
|
||||
|
|
|
@ -8,12 +8,10 @@ import time
|
|||
|
||||
from enum import Enum
|
||||
from PyQt5.QtNetwork import QNetworkRequest, QHttpPart, QHttpMultiPart
|
||||
from PyQt5.QtCore import QUrl, QByteArray, pyqtSlot, pyqtProperty, QCoreApplication, QTimer, pyqtSignal, QObject
|
||||
from PyQt5.QtCore import QUrl, pyqtSlot, pyqtProperty, QCoreApplication, QTimer, pyqtSignal, QObject
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply
|
||||
from PyQt5.QtQml import QQmlComponent, QQmlContext
|
||||
from UM.Application import Application
|
||||
from UM.Decorators import override
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.OutputDevice import OutputDeviceError
|
||||
|
@ -131,7 +129,7 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte
|
|||
@pyqtProperty(QObject, notify=selectedPrinterChanged)
|
||||
def controlItem(self):
|
||||
# TODO: Probably not the nicest way to do this. This needs to be done better at some point in time.
|
||||
if not self._control_component:
|
||||
if not self._control_item:
|
||||
self._createControlViewFromQML()
|
||||
name = self._selected_printer.get("friendly_name")
|
||||
if name == self._automatic_printer.get("friendly_name") or name == "":
|
||||
|
@ -235,17 +233,8 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte
|
|||
|
||||
def spawnPrintView(self):
|
||||
if self._print_view is None:
|
||||
path = QUrl.fromLocalFile(os.path.join(self._plugin_path, "PrintWindow.qml"))
|
||||
component = QQmlComponent(Application.getInstance()._engine, path)
|
||||
|
||||
self._print_context = QQmlContext(Application.getInstance()._engine.rootContext())
|
||||
self._print_context.setContextProperty("OutputDevice", self)
|
||||
self._print_view = component.create(self._print_context)
|
||||
|
||||
if component.isError():
|
||||
Logger.log("e", " Errors creating component: \n%s", "\n".join(
|
||||
[e.toString() for e in component.errors()]))
|
||||
|
||||
path = os.path.join(self._plugin_path, "PrintWindow.qml")
|
||||
self._print_view = Application.getInstance().createQmlComponent(path, {"OutputDevice", self})
|
||||
if self._print_view is not None:
|
||||
self._print_view.show()
|
||||
|
||||
|
@ -263,6 +252,8 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte
|
|||
self._error_message.show()
|
||||
return
|
||||
|
||||
self.writeStarted.emit(self) # Allow postprocessing before sending data to the printer
|
||||
|
||||
if len(self._printers) > 1:
|
||||
self.spawnPrintView() # Ask user how to print it.
|
||||
elif len(self._printers) == 1:
|
||||
|
|
|
@ -13,6 +13,7 @@ from UM.Application import Application
|
|||
from UM.Logger import Logger
|
||||
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
|
||||
from UM.Message import Message
|
||||
from UM.Qt.Duration import DurationFormat
|
||||
|
||||
from PyQt5.QtCore import QUrl, pyqtSlot, pyqtSignal, pyqtProperty
|
||||
|
||||
|
@ -52,6 +53,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||
|
||||
self._heatup_wait_start_time = time.time()
|
||||
|
||||
self.jobStateChanged.connect(self._onJobStateChanged)
|
||||
|
||||
## Queue for commands that need to be send. Used when command is sent when a print is active.
|
||||
self._command_queue = queue.Queue()
|
||||
|
||||
|
@ -60,7 +63,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||
|
||||
## Set when print is started in order to check running time.
|
||||
self._print_start_time = None
|
||||
self._print_start_time_100 = None
|
||||
self._print_estimated_time = None
|
||||
|
||||
## Keep track where in the provided g-code the print is
|
||||
self._gcode_position = 0
|
||||
|
@ -125,6 +128,29 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||
def _homeBed(self):
|
||||
self._sendCommand("G28 Z")
|
||||
|
||||
## Updates the target bed temperature from the printer, and emit a signal if it was changed.
|
||||
#
|
||||
# /param temperature The new target temperature of the bed.
|
||||
# /return boolean, True if the temperature was changed, false if the new temperature has the same value as the already stored temperature
|
||||
def _updateTargetBedTemperature(self, temperature):
|
||||
if self._target_bed_temperature == temperature:
|
||||
return False
|
||||
self._target_bed_temperature = temperature
|
||||
self.targetBedTemperatureChanged.emit()
|
||||
return True
|
||||
|
||||
## Updates the target hotend temperature from the printer, and emit a signal if it was changed.
|
||||
#
|
||||
# /param index The index of the hotend.
|
||||
# /param temperature The new target temperature of the hotend.
|
||||
# /return boolean, True if the temperature was changed, false if the new temperature has the same value as the already stored temperature
|
||||
def _updateTargetHotendTemperature(self, index, temperature):
|
||||
if self._target_hotend_temperatures[index] == temperature:
|
||||
return False
|
||||
self._target_hotend_temperatures[index] = temperature
|
||||
self.targetHotendTemperaturesChanged.emit()
|
||||
return True
|
||||
|
||||
## A name for the device.
|
||||
@pyqtProperty(str, constant = True)
|
||||
def name(self):
|
||||
|
@ -164,7 +190,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||
# Reset line number. If this is not done, first line is sometimes ignored
|
||||
self._gcode.insert(0, "M110")
|
||||
self._gcode_position = 0
|
||||
self._print_start_time_100 = None
|
||||
self._is_printing = True
|
||||
self._print_start_time = time.time()
|
||||
|
||||
|
@ -394,7 +419,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||
self._listen_thread.join()
|
||||
except:
|
||||
pass
|
||||
self._serial.close()
|
||||
if self._serial is not None: # Avoid a race condition when a thread can change the value of self._serial to None
|
||||
self._serial.close()
|
||||
|
||||
self._listen_thread = threading.Thread(target = self._listen)
|
||||
self._listen_thread.daemon = True
|
||||
|
@ -446,7 +472,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||
#
|
||||
# \param nodes A collection of scene nodes to send. This is ignored.
|
||||
# \param file_name \type{string} A suggestion for a file name to write.
|
||||
# This is ignored.
|
||||
# \param filter_by_machine Whether to filter MIME types by machine. This
|
||||
# is ignored.
|
||||
# \param kwargs Keyword arguments.
|
||||
|
@ -462,6 +487,9 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||
self._error_message.show()
|
||||
return
|
||||
|
||||
self.setJobName(file_name)
|
||||
self._print_estimated_time = int(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.Seconds))
|
||||
|
||||
Application.getInstance().showPrintMonitor.emit(True)
|
||||
self.startPrint()
|
||||
|
||||
|
@ -482,6 +510,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||
## Listen thread function.
|
||||
def _listen(self):
|
||||
Logger.log("i", "Printer connection listen thread started for %s" % self._serial_port)
|
||||
container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
temperature_request_timeout = time.time()
|
||||
ok_timeout = time.time()
|
||||
while self._connection_state == ConnectionState.connected:
|
||||
|
@ -511,16 +540,40 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||
self._setErrorState(line[6:])
|
||||
|
||||
elif b" T:" in line or line.startswith(b"T:"): # Temperature message
|
||||
temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line)
|
||||
temperature_set = False
|
||||
try:
|
||||
self._setHotendTemperature(self._temperature_requested_extruder_index, float(re.search(b"T: *([0-9\.]*)", line).group(1)))
|
||||
for match in temperature_matches:
|
||||
if match[0]:
|
||||
extruder_nr = int(match[0])
|
||||
if extruder_nr >= container_stack.getProperty("machine_extruder_count", "value"):
|
||||
continue
|
||||
if match[1]:
|
||||
self._setHotendTemperature(extruder_nr, float(match[1]))
|
||||
temperature_set = True
|
||||
if match[2]:
|
||||
self._updateTargetHotendTemperature(extruder_nr, float(match[2]))
|
||||
else:
|
||||
requested_temperatures = match
|
||||
if not temperature_set and requested_temperatures:
|
||||
if requested_temperatures[1]:
|
||||
self._setHotendTemperature(self._temperature_requested_extruder_index, float(requested_temperatures[1]))
|
||||
if requested_temperatures[2]:
|
||||
self._updateTargetHotendTemperature(self._temperature_requested_extruder_index, float(requested_temperatures[2]))
|
||||
except:
|
||||
pass
|
||||
if b"B:" in line: # Check if it's a bed temperature
|
||||
Logger.log("w", "Could not parse hotend temperatures from response: %s", line)
|
||||
# Check if there's also a bed temperature
|
||||
temperature_matches = re.findall(b"B: ?([\d\.]+) ?\/?([\d\.]+)?", line)
|
||||
if container_stack.getProperty("machine_heated_bed", "value") and len(temperature_matches) > 0:
|
||||
match = temperature_matches[0]
|
||||
try:
|
||||
self._setBedTemperature(float(re.search(b"B: *([0-9\.]*)", line).group(1)))
|
||||
except Exception as e:
|
||||
pass
|
||||
#TODO: temperature changed callback
|
||||
if match[0]:
|
||||
self._setBedTemperature(float(match[0]))
|
||||
if match[1]:
|
||||
self._updateTargetBedTemperature(float(match[1]))
|
||||
except:
|
||||
Logger.log("w", "Could not parse bed temperature from response: %s", line)
|
||||
|
||||
elif b"_min" in line or b"_max" in line:
|
||||
tag, value = line.split(b":", 1)
|
||||
self._setEndstopState(tag,(b"H" in value or b"TRIGGERED" in value))
|
||||
|
@ -559,8 +612,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||
def _sendNextGcodeLine(self):
|
||||
if self._gcode_position >= len(self._gcode):
|
||||
return
|
||||
if self._gcode_position == 100:
|
||||
self._print_start_time_100 = time.time()
|
||||
line = self._gcode[self._gcode_position]
|
||||
|
||||
if ";" in line:
|
||||
|
@ -584,8 +635,18 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||
checksum = functools.reduce(lambda x,y: x^y, map(ord, "N%d%s" % (self._gcode_position, line)))
|
||||
|
||||
self._sendCommand("N%d%s*%d" % (self._gcode_position, line, checksum))
|
||||
|
||||
progress = (self._gcode_position / len(self._gcode))
|
||||
|
||||
elapsed_time = int(time.time() - self._print_start_time)
|
||||
self.setTimeElapsed(elapsed_time)
|
||||
estimated_time = self._print_estimated_time
|
||||
if progress > .1:
|
||||
estimated_time = self._print_estimated_time * (1-progress) + elapsed_time
|
||||
self.setTimeTotal(estimated_time)
|
||||
|
||||
self._gcode_position += 1
|
||||
self.setProgress((self._gcode_position / len(self._gcode)) * 100)
|
||||
self.setProgress(progress * 100)
|
||||
self.progressChanged.emit()
|
||||
|
||||
## Set the state of the print.
|
||||
|
@ -600,6 +661,13 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||
elif job_state == "abort":
|
||||
self.cancelPrint()
|
||||
|
||||
def _onJobStateChanged(self):
|
||||
# clear the job name & times when printing is done or aborted
|
||||
if self._job_state == "ready":
|
||||
self.setJobName("")
|
||||
self.setTimeElapsed(0)
|
||||
self.setTimeTotal(0)
|
||||
|
||||
## Set the progress of the print.
|
||||
# It will be normalized (based on max_progress) to range 0 - 100
|
||||
def setProgress(self, progress, max_progress = 100):
|
||||
|
|
|
@ -16,13 +16,11 @@ from cura.CuraApplication import CuraApplication
|
|||
|
||||
import threading
|
||||
import platform
|
||||
import glob
|
||||
import time
|
||||
import os.path
|
||||
import serial.tools.list_ports
|
||||
from UM.Extension import Extension
|
||||
|
||||
from PyQt5.QtQml import QQmlComponent, QQmlContext
|
||||
from PyQt5.QtCore import QUrl, QObject, pyqtSlot, pyqtProperty, pyqtSignal, Qt
|
||||
from UM.i18n import i18nCatalog
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
@ -99,7 +97,8 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
|
|||
@pyqtSlot(str)
|
||||
def updateAllFirmware(self, file_name):
|
||||
if file_name.startswith("file://"):
|
||||
file_name = QUrl(file_name).toLocalFile() # File dialogs prepend the path with file://, which we don't need / want
|
||||
file_name = QUrl(file_name).toLocalFile() # File dialogs prepend the path with file://, which we don't need / want
|
||||
|
||||
if not self._usb_output_devices:
|
||||
Message(i18n_catalog.i18nc("@info", "Unable to update firmware because there are no printers connected."), title = i18n_catalog.i18nc("@info:title", "Warning")).show()
|
||||
return
|
||||
|
|
|
@ -38,7 +38,6 @@ class UM2UpgradeSelection(MachineAction):
|
|||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
variant_container = global_container_stack.extruders["0"].variant
|
||||
variant_index = global_container_stack.getContainerIndex(variant_container)
|
||||
|
||||
if has_variants:
|
||||
if "has_variants" in global_container_stack.getMetaData():
|
||||
|
@ -60,6 +59,6 @@ class UM2UpgradeSelection(MachineAction):
|
|||
global_container_stack.removeMetaDataEntry("has_variants")
|
||||
|
||||
# Set the variant container to an empty variant
|
||||
global_container_stack.variant = ContainerRegistry.getInstance().getEmptyInstanceContainer()
|
||||
global_container_stack.extruders["0"].variant = ContainerRegistry.getInstance().getEmptyInstanceContainer()
|
||||
|
||||
Application.getInstance().globalContainerStackChanged.emit()
|
||||
|
|
|
@ -9,13 +9,12 @@ from UM.Logger import Logger
|
|||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
from PyQt5.QtQml import QQmlComponent, QQmlContext
|
||||
from PyQt5.QtCore import QUrl, QObject, pyqtSlot
|
||||
from PyQt5.QtCore import QObject, pyqtSlot
|
||||
|
||||
import os.path
|
||||
|
||||
class UserAgreement(QObject, Extension):
|
||||
def __init__(self, parent = None):
|
||||
def __init__(self):
|
||||
super(UserAgreement, self).__init__()
|
||||
self._user_agreement_window = None
|
||||
self._user_agreement_context = None
|
||||
|
@ -33,8 +32,8 @@ class UserAgreement(QObject, Extension):
|
|||
self._user_agreement_window.show()
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def didAgree(self, userChoice):
|
||||
if userChoice:
|
||||
def didAgree(self, user_choice):
|
||||
if user_choice:
|
||||
Logger.log("i", "User agreed to the user agreement")
|
||||
Preferences.getInstance().setValue("general/accepted_user_agreement", True)
|
||||
self._user_agreement_window.hide()
|
||||
|
|
|
@ -43,7 +43,7 @@ UM.Dialog
|
|||
anchors.right: parent.right
|
||||
text: catalog.i18nc("@action:button", "I understand and agree")
|
||||
onClicked: {
|
||||
manager.didAgree(true)
|
||||
baseDialog.accepted()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,13 +52,12 @@ UM.Dialog
|
|||
anchors.left: parent.left
|
||||
text: catalog.i18nc("@action:button", "I don't agree")
|
||||
onClicked: {
|
||||
manager.didAgree(false)
|
||||
baseDialog.rejected()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onClosing: {
|
||||
manager.didAgree(false)
|
||||
}
|
||||
onAccepted: manager.didAgree(true)
|
||||
onRejected: manager.didAgree(false)
|
||||
onClosing: manager.didAgree(false)
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@ class Profile:
|
|||
|
||||
config.add_section("metadata")
|
||||
config.set("metadata", "quality_type", "normal") #This feature doesn't exist in 2.1 yet, so we don't know the actual quality type. For now, always base it on normal.
|
||||
config.set("metadata", "type", "quality_changes")
|
||||
config.set("metadata", "type", "quality")
|
||||
if self._weight:
|
||||
config.set("metadata", "weight", str(self._weight))
|
||||
if self._machine_variant_name:
|
||||
|
|
|
@ -203,6 +203,14 @@ class VersionUpgrade30to31(VersionUpgrade):
|
|||
def _createExtruderQualityChangesForSingleExtrusionMachine(self, filename, global_quality_changes):
|
||||
suffix = "_" + quote_plus(global_quality_changes["general"]["name"].lower())
|
||||
machine_name = os.path.os.path.basename(filename).replace(".inst.cfg", "").replace(suffix, "")
|
||||
|
||||
# Why is this here?!
|
||||
# When we load a .curaprofile file the deserialize will trigger a version upgrade, creating a dangling file.
|
||||
# This file can be recognized by it's lack of a machine name in the target filename.
|
||||
# So when we detect that situation here, we don't create the file and return.
|
||||
if machine_name == "":
|
||||
return
|
||||
|
||||
new_filename = machine_name + "_" + "fdmextruder" + suffix
|
||||
|
||||
extruder_quality_changes_parser = configparser.ConfigParser()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue