mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-08-06 05:23:58 -06:00
Merge branch 'master' of github.com:Ultimaker/Cura into network_rewrite
This commit is contained in:
commit
ed9634ebe0
77 changed files with 2675 additions and 791 deletions
|
@ -4,7 +4,6 @@
|
|||
import os.path
|
||||
import zipfile
|
||||
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
from UM.Math.Matrix import Matrix
|
||||
from UM.Math.Vector import Vector
|
||||
|
@ -15,9 +14,10 @@ from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
|||
from UM.Application import Application
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from cura.QualityManager import QualityManager
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from cura.SliceableObjectDecorator import SliceableObjectDecorator
|
||||
from cura.ZOffsetDecorator import ZOffsetDecorator
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||
from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
|
||||
|
||||
MYPY = False
|
||||
|
||||
|
@ -43,6 +43,7 @@ class ThreeMFReader(MeshReader):
|
|||
}
|
||||
self._base_name = ""
|
||||
self._unit = None
|
||||
self._object_count = 0 # Used to name objects as there is no node name yet.
|
||||
|
||||
def _createMatrixFromTransformationString(self, transformation):
|
||||
if transformation == "":
|
||||
|
@ -77,7 +78,12 @@ class ThreeMFReader(MeshReader):
|
|||
## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a Uranium scene node.
|
||||
# \returns Uranium scene node.
|
||||
def _convertSavitarNodeToUMNode(self, savitar_node):
|
||||
um_node = SceneNode()
|
||||
self._object_count += 1
|
||||
node_name = "Object %s" % self._object_count
|
||||
|
||||
um_node = CuraSceneNode()
|
||||
um_node.addDecorator(BuildPlateDecorator(0))
|
||||
um_node.setName(node_name)
|
||||
transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
|
||||
um_node.setTransformation(transformation)
|
||||
mesh_builder = MeshBuilder()
|
||||
|
@ -147,6 +153,7 @@ class ThreeMFReader(MeshReader):
|
|||
|
||||
def read(self, file_name):
|
||||
result = []
|
||||
self._object_count = 0 # Used to name objects as there is no node name yet.
|
||||
# The base object of 3mf is a zipped archive.
|
||||
try:
|
||||
archive = zipfile.ZipFile(file_name, "r")
|
||||
|
|
|
@ -7,6 +7,7 @@ from UM.Logger import Logger
|
|||
from UM.Math.Matrix import Matrix
|
||||
from UM.Application import Application
|
||||
import UM.Scene.SceneNode
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
|
||||
import Savitar
|
||||
|
||||
|
@ -63,7 +64,7 @@ class ThreeMFWriter(MeshWriter):
|
|||
## Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
|
||||
# \returns Uranium Scenen node.
|
||||
def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()):
|
||||
if type(um_node) is not UM.Scene.SceneNode.SceneNode:
|
||||
if type(um_node) not in [UM.Scene.SceneNode.SceneNode, CuraSceneNode]:
|
||||
return None
|
||||
|
||||
savitar_node = Savitar.SceneNode()
|
||||
|
|
|
@ -16,6 +16,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
|||
from UM.Qt.Duration import DurationFormat
|
||||
from PyQt5.QtCore import QObject, pyqtSlot
|
||||
|
||||
from collections import defaultdict
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from . import ProcessSlicedLayersJob
|
||||
from . import StartSliceJob
|
||||
|
@ -69,9 +70,10 @@ class CuraEngineBackend(QObject, Backend):
|
|||
# Workaround to disable layer view processing if layer view is not active.
|
||||
self._layer_view_active = False
|
||||
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
|
||||
Application.getInstance().getBuildPlateModel().activeBuildPlateChanged.connect(self._onActiveViewChanged)
|
||||
self._onActiveViewChanged()
|
||||
self._stored_layer_data = []
|
||||
self._stored_optimized_layer_data = []
|
||||
self._stored_optimized_layer_data = {} # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
|
||||
|
||||
self._scene = Application.getInstance().getController().getScene()
|
||||
self._scene.sceneChanged.connect(self._onSceneChanged)
|
||||
|
@ -105,17 +107,18 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage
|
||||
|
||||
self._start_slice_job = None
|
||||
self._start_slice_job_build_plate = None
|
||||
self._slicing = False # Are we currently slicing?
|
||||
self._restart = False # Back-end is currently restarting?
|
||||
self._tool_active = False # If a tool is active, some tasks do not have to do anything
|
||||
self._always_restart = True # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness.
|
||||
self._process_layers_job = None # The currently active job to process layers, or None if it is not processing layers.
|
||||
self._need_slicing = False
|
||||
self._build_plates_to_be_sliced = [] # what needs slicing?
|
||||
self._engine_is_fresh = True # Is the newly started engine used before or not?
|
||||
|
||||
self._backend_log_max_lines = 20000 # Maximum number of lines to buffer
|
||||
self._error_message = None # Pop-up message that shows errors.
|
||||
self._last_num_objects = 0 # Count number of objects to see if there is something changed
|
||||
self._last_num_objects = defaultdict(int) # Count number of objects to see if there is something changed
|
||||
self._postponed_scene_change_sources = [] # scene change is postponed (by a tool)
|
||||
|
||||
self.backendQuit.connect(self._onBackendQuit)
|
||||
|
@ -174,6 +177,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self._createSocket()
|
||||
|
||||
if self._process_layers_job: # We were processing layers. Stop that, the layers are going to change soon.
|
||||
Logger.log("d", "Aborting process layers job...")
|
||||
self._process_layers_job.abort()
|
||||
self._process_layers_job = None
|
||||
|
||||
|
@ -190,17 +194,35 @@ class CuraEngineBackend(QObject, Backend):
|
|||
|
||||
## Perform a slice of the scene.
|
||||
def slice(self):
|
||||
Logger.log("d", "starting to slice!")
|
||||
self._slice_start_time = time()
|
||||
if not self._need_slicing:
|
||||
if not self._build_plates_to_be_sliced:
|
||||
self.processingProgress.emit(1.0)
|
||||
self.backendStateChange.emit(BackendState.Done)
|
||||
Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.")
|
||||
return
|
||||
if Application.getInstance().getPrintInformation():
|
||||
Application.getInstance().getPrintInformation().setToZeroPrintInformation()
|
||||
|
||||
if self._process_layers_job:
|
||||
Logger.log("d", " ## Process layers job still busy, trying later")
|
||||
return
|
||||
|
||||
if not hasattr(self._scene, "gcode_list"):
|
||||
self._scene.gcode_list = {}
|
||||
|
||||
# see if we really have to slice
|
||||
active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
|
||||
build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0)
|
||||
Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced)
|
||||
num_objects = self._numObjects()
|
||||
if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0:
|
||||
self._scene.gcode_list[build_plate_to_be_sliced] = []
|
||||
Logger.log("d", "Build plate %s has 0 objects to be sliced, skipping", build_plate_to_be_sliced)
|
||||
return
|
||||
|
||||
self._stored_layer_data = []
|
||||
self._stored_optimized_layer_data = []
|
||||
self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
|
||||
|
||||
if Application.getInstance().getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
|
||||
Application.getInstance().getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
|
||||
|
||||
if self._process is None:
|
||||
self._createSocket()
|
||||
|
@ -210,12 +232,14 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self.processingProgress.emit(0.0)
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
|
||||
self._scene.gcode_list = []
|
||||
self._scene.gcode_list[build_plate_to_be_sliced] = [] #[] indexed by build plate number
|
||||
self._slicing = True
|
||||
self.slicingStarted.emit()
|
||||
|
||||
slice_message = self._socket.createMessage("cura.proto.Slice")
|
||||
self._start_slice_job = StartSliceJob.StartSliceJob(slice_message)
|
||||
self._start_slice_job_build_plate = build_plate_to_be_sliced
|
||||
self._start_slice_job.setBuildPlate(self._start_slice_job_build_plate)
|
||||
self._start_slice_job.start()
|
||||
self._start_slice_job.finished.connect(self._onStartSliceCompleted)
|
||||
|
||||
|
@ -224,7 +248,8 @@ class CuraEngineBackend(QObject, Backend):
|
|||
def _terminate(self):
|
||||
self._slicing = False
|
||||
self._stored_layer_data = []
|
||||
self._stored_optimized_layer_data = []
|
||||
if self._start_slice_job_build_plate in self._stored_optimized_layer_data:
|
||||
del self._stored_optimized_layer_data[self._start_slice_job_build_plate]
|
||||
if self._start_slice_job is not None:
|
||||
self._start_slice_job.cancel()
|
||||
|
||||
|
@ -339,7 +364,10 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self.backendStateChange.emit(BackendState.Error)
|
||||
else:
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
pass
|
||||
self._invokeSlice()
|
||||
return
|
||||
|
||||
# Preparation completed, send it to the backend.
|
||||
self._socket.sendMessage(job.getSliceMessage())
|
||||
|
||||
|
@ -363,7 +391,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self.backendStateChange.emit(BackendState.Disabled)
|
||||
gcode_list = node.callDecoration("getGCodeList")
|
||||
if gcode_list is not None:
|
||||
self._scene.gcode_list = gcode_list
|
||||
self._scene.gcode_list[node.callDecoration("getBuildPlateNumber")] = gcode_list
|
||||
|
||||
if self._use_timer == enable_timer:
|
||||
return self._use_timer
|
||||
|
@ -375,33 +403,48 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self.disableTimer()
|
||||
return False
|
||||
|
||||
## Return a dict with number of objects per build plate
|
||||
def _numObjects(self):
|
||||
num_objects = defaultdict(int)
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
# Only count sliceable objects
|
||||
if node.callDecoration("isSliceable"):
|
||||
build_plate_number = node.callDecoration("getBuildPlateNumber")
|
||||
num_objects[build_plate_number] += 1
|
||||
return num_objects
|
||||
|
||||
## Listener for when the scene has changed.
|
||||
#
|
||||
# This should start a slice if the scene is now ready to slice.
|
||||
#
|
||||
# \param source The scene node that was changed.
|
||||
def _onSceneChanged(self, source):
|
||||
if type(source) is not SceneNode:
|
||||
if not issubclass(type(source), SceneNode):
|
||||
return
|
||||
|
||||
root_scene_nodes_changed = False
|
||||
build_plate_changed = set()
|
||||
source_build_plate_number = source.callDecoration("getBuildPlateNumber")
|
||||
if source == self._scene.getRoot():
|
||||
num_objects = 0
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
# Only count sliceable objects
|
||||
if node.callDecoration("isSliceable"):
|
||||
num_objects += 1
|
||||
if num_objects != self._last_num_objects:
|
||||
self._last_num_objects = num_objects
|
||||
root_scene_nodes_changed = True
|
||||
else:
|
||||
return
|
||||
# we got the root node
|
||||
num_objects = self._numObjects()
|
||||
for build_plate_number in list(self._last_num_objects.keys()) + list(num_objects.keys()):
|
||||
if build_plate_number not in self._last_num_objects or num_objects[build_plate_number] != self._last_num_objects[build_plate_number]:
|
||||
self._last_num_objects[build_plate_number] = num_objects[build_plate_number]
|
||||
build_plate_changed.add(build_plate_number)
|
||||
else:
|
||||
# we got a single scenenode
|
||||
if not source.callDecoration("isGroup"):
|
||||
if source.getMeshData() is None:
|
||||
return
|
||||
if source.getMeshData().getVertices() is None:
|
||||
return
|
||||
|
||||
if not source.callDecoration("isGroup") and not root_scene_nodes_changed:
|
||||
if source.getMeshData() is None:
|
||||
return
|
||||
if source.getMeshData().getVertices() is None:
|
||||
return
|
||||
build_plate_changed.add(source_build_plate_number)
|
||||
|
||||
build_plate_changed.discard(None)
|
||||
build_plate_changed.discard(-1) # object not on build plate
|
||||
if not build_plate_changed:
|
||||
return
|
||||
|
||||
if self._tool_active:
|
||||
# do it later, each source only has to be done once
|
||||
|
@ -409,9 +452,17 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self._postponed_scene_change_sources.append(source)
|
||||
return
|
||||
|
||||
self.needsSlicing()
|
||||
self.stopSlicing()
|
||||
self._onChanged()
|
||||
for build_plate_number in build_plate_changed:
|
||||
if build_plate_number not in self._build_plates_to_be_sliced:
|
||||
self._build_plates_to_be_sliced.append(build_plate_number)
|
||||
self.processingProgress.emit(0.0)
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
# if not self._use_timer:
|
||||
# With manually having to slice, we want to clear the old invalid layer data.
|
||||
self._clearLayerData(build_plate_changed)
|
||||
|
||||
self._invokeSlice()
|
||||
|
||||
## Called when an error occurs in the socket connection towards the engine.
|
||||
#
|
||||
|
@ -431,16 +482,21 @@ class CuraEngineBackend(QObject, Backend):
|
|||
Logger.log("w", "A socket error caused the connection to be reset")
|
||||
|
||||
## Remove old layer data (if any)
|
||||
def _clearLayerData(self):
|
||||
def _clearLayerData(self, build_plate_numbers = set()):
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if node.callDecoration("getLayerData"):
|
||||
node.getParent().removeChild(node)
|
||||
break
|
||||
if not build_plate_numbers or node.callDecoration("getBuildPlateNumber") in build_plate_numbers:
|
||||
node.getParent().removeChild(node)
|
||||
|
||||
## Convenient function: set need_slicing, emit state and clear layer data
|
||||
def markSliceAll(self):
|
||||
for build_plate_number in range(Application.getInstance().getBuildPlateModel().maxBuildPlate + 1):
|
||||
if build_plate_number not in self._build_plates_to_be_sliced:
|
||||
self._build_plates_to_be_sliced.append(build_plate_number)
|
||||
|
||||
## Convenient function: mark everything to slice, emit state and clear layer data
|
||||
def needsSlicing(self):
|
||||
self.stopSlicing()
|
||||
self._need_slicing = True
|
||||
self.markSliceAll()
|
||||
self.processingProgress.emit(0.0)
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
if not self._use_timer:
|
||||
|
@ -462,7 +518,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
|
||||
def _onStackErrorCheckFinished(self):
|
||||
self._is_error_check_scheduled = False
|
||||
if not self._slicing and self._need_slicing:
|
||||
if not self._slicing and self._build_plates_to_be_sliced: #self._need_slicing:
|
||||
self.needsSlicing()
|
||||
self._onChanged()
|
||||
|
||||
|
@ -476,7 +532,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
#
|
||||
# \param message The protobuf message containing sliced layer data.
|
||||
def _onOptimizedLayerMessage(self, message):
|
||||
self._stored_optimized_layer_data.append(message)
|
||||
self._stored_optimized_layer_data[self._start_slice_job_build_plate].append(message)
|
||||
|
||||
## Called when a progress message is received from the engine.
|
||||
#
|
||||
|
@ -485,6 +541,16 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self.processingProgress.emit(message.amount)
|
||||
self.backendStateChange.emit(BackendState.Processing)
|
||||
|
||||
# testing
|
||||
def _invokeSlice(self):
|
||||
if self._use_timer:
|
||||
# if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
|
||||
# otherwise business as usual
|
||||
if self._is_error_check_scheduled:
|
||||
self._change_timer.stop()
|
||||
else:
|
||||
self._change_timer.start()
|
||||
|
||||
## Called when the engine sends a message that slicing is finished.
|
||||
#
|
||||
# \param message The protobuf message signalling that slicing is finished.
|
||||
|
@ -492,36 +558,44 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self.backendStateChange.emit(BackendState.Done)
|
||||
self.processingProgress.emit(1.0)
|
||||
|
||||
for line in self._scene.gcode_list:
|
||||
gcode_list = self._scene.gcode_list[self._start_slice_job_build_plate]
|
||||
for index, line in enumerate(gcode_list):
|
||||
replaced = line.replace("{print_time}", str(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601)))
|
||||
replaced = replaced.replace("{filament_amount}", str(Application.getInstance().getPrintInformation().materialLengths))
|
||||
replaced = replaced.replace("{filament_weight}", str(Application.getInstance().getPrintInformation().materialWeights))
|
||||
replaced = replaced.replace("{filament_cost}", str(Application.getInstance().getPrintInformation().materialCosts))
|
||||
replaced = replaced.replace("{jobname}", str(Application.getInstance().getPrintInformation().jobName))
|
||||
|
||||
self._scene.gcode_list[self._scene.gcode_list.index(line)] = replaced
|
||||
gcode_list[index] = replaced
|
||||
|
||||
self._slicing = False
|
||||
self._need_slicing = False
|
||||
Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time )
|
||||
if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()):
|
||||
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data)
|
||||
self._process_layers_job.finished.connect(self._onProcessLayersFinished)
|
||||
self._process_layers_job.start()
|
||||
self._stored_optimized_layer_data = []
|
||||
|
||||
# See if we need to process the sliced layers job.
|
||||
active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
|
||||
if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()) and active_build_plate == self._start_slice_job_build_plate:
|
||||
self._startProcessSlicedLayersJob(active_build_plate)
|
||||
# self._onActiveViewChanged()
|
||||
self._start_slice_job_build_plate = None
|
||||
|
||||
Logger.log("d", "See if there is more to slice...")
|
||||
# Somehow this results in an Arcus Error
|
||||
# self.slice()
|
||||
# Testing call slice again, allow backend to restart by using the timer
|
||||
self._invokeSlice()
|
||||
|
||||
## Called when a g-code message is received from the engine.
|
||||
#
|
||||
# \param message The protobuf message containing g-code, encoded as UTF-8.
|
||||
def _onGCodeLayerMessage(self, message):
|
||||
self._scene.gcode_list.append(message.data.decode("utf-8", "replace"))
|
||||
self._scene.gcode_list[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace"))
|
||||
|
||||
## Called when a g-code prefix message is received from the engine.
|
||||
#
|
||||
# \param message The protobuf message containing the g-code prefix,
|
||||
# encoded as UTF-8.
|
||||
def _onGCodePrefixMessage(self, message):
|
||||
self._scene.gcode_list.insert(0, message.data.decode("utf-8", "replace"))
|
||||
self._scene.gcode_list[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace"))
|
||||
|
||||
## Creates a new socket connection.
|
||||
def _createSocket(self):
|
||||
|
@ -551,7 +625,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount)
|
||||
|
||||
times = self._parseMessagePrintTimes(message)
|
||||
self.printDurationMessage.emit(times, material_amounts)
|
||||
self.printDurationMessage.emit(self._start_slice_job_build_plate, times, material_amounts)
|
||||
|
||||
## Called for parsing message to retrieve estimated time per feature
|
||||
#
|
||||
|
@ -605,19 +679,25 @@ class CuraEngineBackend(QObject, Backend):
|
|||
source = self._postponed_scene_change_sources.pop(0)
|
||||
self._onSceneChanged(source)
|
||||
|
||||
def _startProcessSlicedLayersJob(self, build_plate_number):
|
||||
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data[build_plate_number])
|
||||
self._process_layers_job.setBuildPlate(build_plate_number)
|
||||
self._process_layers_job.finished.connect(self._onProcessLayersFinished)
|
||||
self._process_layers_job.start()
|
||||
|
||||
## Called when the user changes the active view mode.
|
||||
def _onActiveViewChanged(self):
|
||||
if Application.getInstance().getController().getActiveView():
|
||||
view = Application.getInstance().getController().getActiveView()
|
||||
application = Application.getInstance()
|
||||
view = application.getController().getActiveView()
|
||||
if view:
|
||||
active_build_plate = application.getBuildPlateModel().activeBuildPlate
|
||||
if view.getPluginId() == "SimulationView": # If switching to layer view, we should process the layers if that hasn't been done yet.
|
||||
self._layer_view_active = True
|
||||
# There is data and we're not slicing at the moment
|
||||
# if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment.
|
||||
if self._stored_optimized_layer_data and not self._slicing:
|
||||
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data)
|
||||
self._process_layers_job.finished.connect(self._onProcessLayersFinished)
|
||||
self._process_layers_job.start()
|
||||
self._stored_optimized_layer_data = []
|
||||
# TODO: what build plate I am slicing
|
||||
if active_build_plate in self._stored_optimized_layer_data and not self._slicing and not self._process_layers_job:
|
||||
self._startProcessSlicedLayersJob(active_build_plate)
|
||||
else:
|
||||
self._layer_view_active = False
|
||||
|
||||
|
@ -653,7 +733,10 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self._onChanged()
|
||||
|
||||
def _onProcessLayersFinished(self, job):
|
||||
del self._stored_optimized_layer_data[job.getBuildPlate()]
|
||||
self._process_layers_job = None
|
||||
Logger.log("d", "See if there is more to slice(2)...")
|
||||
self._invokeSlice()
|
||||
|
||||
## Connect slice function to timer.
|
||||
def enableTimer(self):
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
import gc
|
||||
|
||||
from UM.Job import Job
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Application import Application
|
||||
from UM.Mesh.MeshData import MeshData
|
||||
|
@ -17,6 +16,7 @@ from UM.Logger import Logger
|
|||
|
||||
from UM.Math.Vector import Vector
|
||||
|
||||
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from cura import LayerDataBuilder
|
||||
from cura import LayerDataDecorator
|
||||
|
@ -49,6 +49,7 @@ class ProcessSlicedLayersJob(Job):
|
|||
self._scene = Application.getInstance().getController().getScene()
|
||||
self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1)
|
||||
self._abort_requested = False
|
||||
self._build_plate_number = None
|
||||
|
||||
## Aborts the processing of layers.
|
||||
#
|
||||
|
@ -59,7 +60,14 @@ class ProcessSlicedLayersJob(Job):
|
|||
def abort(self):
|
||||
self._abort_requested = True
|
||||
|
||||
def setBuildPlate(self, new_value):
|
||||
self._build_plate_number = new_value
|
||||
|
||||
def getBuildPlate(self):
|
||||
return self._build_plate_number
|
||||
|
||||
def run(self):
|
||||
Logger.log("d", "Processing new layer for build plate %s..." % self._build_plate_number)
|
||||
start_time = time()
|
||||
view = Application.getInstance().getController().getActiveView()
|
||||
if view.getPluginId() == "SimulationView":
|
||||
|
@ -74,16 +82,7 @@ class ProcessSlicedLayersJob(Job):
|
|||
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
|
||||
|
||||
new_node = SceneNode()
|
||||
|
||||
## Remove old layer data (if any)
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if node.callDecoration("getLayerData"):
|
||||
node.getParent().removeChild(node)
|
||||
break
|
||||
if self._abort_requested:
|
||||
if self._progress_message:
|
||||
self._progress_message.hide()
|
||||
return
|
||||
new_node.addDecorator(BuildPlateDecorator(self._build_plate_number))
|
||||
|
||||
# Force garbage collection.
|
||||
# For some reason, Python has a tendency to keep the layer data
|
||||
|
|
|
@ -10,12 +10,12 @@ from UM.Job import Job
|
|||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
|
||||
from UM.Settings.Validator import ValidatorState
|
||||
from UM.Settings.SettingRelation import RelationType
|
||||
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode
|
||||
from cura.OneAtATimeIterator import OneAtATimeIterator
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
|
@ -36,9 +36,32 @@ class StartJobResult(IntEnum):
|
|||
## Formatter class that handles token expansion in start/end gcod
|
||||
class GcodeStartEndFormatter(Formatter):
|
||||
def get_value(self, key, args, kwargs): # [CodeStyle: get_value is an overridden function from the Formatter class]
|
||||
# The kwargs dictionary contains a dictionary for each stack (with a string of the extruder_nr as their key),
|
||||
# and a default_extruder_nr to use when no extruder_nr is specified
|
||||
|
||||
if isinstance(key, str):
|
||||
try:
|
||||
return kwargs[key]
|
||||
extruder_nr = kwargs["default_extruder_nr"]
|
||||
except ValueError:
|
||||
extruder_nr = -1
|
||||
|
||||
key_fragments = [fragment.strip() for fragment in key.split(',')]
|
||||
if len(key_fragments) == 2:
|
||||
try:
|
||||
extruder_nr = int(key_fragments[1])
|
||||
except ValueError:
|
||||
try:
|
||||
extruder_nr = int(kwargs["-1"][key_fragments[1]]) # get extruder_nr values from the global stack
|
||||
except (KeyError, ValueError):
|
||||
# either the key does not exist, or the value is not an int
|
||||
Logger.log("w", "Unable to determine stack nr '%s' for key '%s' in start/end gcode, using global stack", key_fragments[1], key_fragments[0])
|
||||
elif len(key_fragments) != 1:
|
||||
Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end gcode", key)
|
||||
return "{" + str(key) + "}"
|
||||
|
||||
key = key_fragments[0]
|
||||
try:
|
||||
return kwargs[str(extruder_nr)][key]
|
||||
except KeyError:
|
||||
Logger.log("w", "Unable to replace '%s' placeholder in start/end gcode", key)
|
||||
return "{" + key + "}"
|
||||
|
@ -55,10 +78,16 @@ class StartSliceJob(Job):
|
|||
self._scene = Application.getInstance().getController().getScene()
|
||||
self._slice_message = slice_message
|
||||
self._is_cancelled = False
|
||||
self._build_plate_number = None
|
||||
|
||||
self._all_extruders_settings = None # cache for all setting values from all stacks (global & extruder) for the current machine
|
||||
|
||||
def getSliceMessage(self):
|
||||
return self._slice_message
|
||||
|
||||
def setBuildPlate(self, build_plate_number):
|
||||
self._build_plate_number = build_plate_number
|
||||
|
||||
## Check if a stack has any errors.
|
||||
## returns true if it has errors, false otherwise.
|
||||
def _checkStackForErrors(self, stack):
|
||||
|
@ -75,6 +104,10 @@ class StartSliceJob(Job):
|
|||
|
||||
## Runs the job that initiates the slicing.
|
||||
def run(self):
|
||||
if self._build_plate_number is None:
|
||||
self.setResult(StartJobResult.Error)
|
||||
return
|
||||
|
||||
stack = Application.getInstance().getGlobalContainerStack()
|
||||
if not stack:
|
||||
self.setResult(StartJobResult.Error)
|
||||
|
@ -108,7 +141,7 @@ class StartSliceJob(Job):
|
|||
with self._scene.getSceneLock():
|
||||
# Remove old layer data.
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if node.callDecoration("getLayerData"):
|
||||
if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number:
|
||||
node.getParent().removeChild(node)
|
||||
break
|
||||
|
||||
|
@ -143,10 +176,11 @@ class StartSliceJob(Job):
|
|||
if per_object_stack:
|
||||
is_non_printing_mesh = any(per_object_stack.getProperty(key, "value") for key in NON_PRINTING_MESH_SETTINGS)
|
||||
|
||||
if not getattr(node, "_outside_buildarea", False) or is_non_printing_mesh:
|
||||
temp_list.append(node)
|
||||
if not is_non_printing_mesh:
|
||||
has_printing_mesh = True
|
||||
if (node.callDecoration("getBuildPlateNumber") == self._build_plate_number):
|
||||
if not getattr(node, "_outside_buildarea", False) or is_non_printing_mesh:
|
||||
temp_list.append(node)
|
||||
if not is_non_printing_mesh:
|
||||
has_printing_mesh = True
|
||||
|
||||
Job.yieldThread()
|
||||
|
||||
|
@ -233,16 +267,33 @@ class StartSliceJob(Job):
|
|||
result["date"] = time.strftime("%d-%m-%Y")
|
||||
result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))]
|
||||
|
||||
initial_extruder_stack = Application.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
|
||||
initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value")
|
||||
result["initial_extruder_nr"] = initial_extruder_nr
|
||||
|
||||
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):
|
||||
# \param default_extruder_nr Stack nr to use when no stack nr is specified, defaults to the global stack
|
||||
def _expandGcodeTokens(self, value: str, default_extruder_nr: int = -1):
|
||||
if not self._all_extruders_settings:
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
|
||||
# NB: keys must be strings for the string formatter
|
||||
self._all_extruders_settings = {
|
||||
"-1": self._buildReplacementTokens(global_stack)
|
||||
}
|
||||
|
||||
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()):
|
||||
extruder_nr = extruder_stack.getProperty("extruder_nr", "value")
|
||||
self._all_extruders_settings[str(extruder_nr)] = self._buildReplacementTokens(extruder_stack)
|
||||
|
||||
try:
|
||||
# any setting can be used as a token
|
||||
fmt = GcodeStartEndFormatter()
|
||||
settings = self._all_extruders_settings.copy()
|
||||
settings["default_extruder_nr"] = default_extruder_nr
|
||||
return str(fmt.format(value, **settings))
|
||||
except:
|
||||
Logger.logException("w", "Unable to do token replacement on start/end gcode")
|
||||
|
@ -259,8 +310,9 @@ class StartSliceJob(Job):
|
|||
settings["material_guid"] = stack.material.getMetaDataEntry("GUID", "")
|
||||
|
||||
# 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)
|
||||
extruder_nr = stack.getProperty("extruder_nr", "value")
|
||||
settings["machine_extruder_start_code"] = self._expandGcodeTokens(settings["machine_extruder_start_code"], extruder_nr)
|
||||
settings["machine_extruder_end_code"] = self._expandGcodeTokens(settings["machine_extruder_end_code"], extruder_nr)
|
||||
|
||||
for key, value in settings.items():
|
||||
# Do not send settings that are not settable_per_extruder.
|
||||
|
@ -285,13 +337,13 @@ 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))
|
||||
|
||||
# Find the correct temperatures from the first used extruder
|
||||
extruder_stack = Application.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
|
||||
extruder_0_settings = self._buildReplacementTokens(extruder_stack)
|
||||
|
||||
# Replace the setting tokens in start and end g-code.
|
||||
settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], extruder_0_settings)
|
||||
settings["machine_end_gcode"] = self._expandGcodeTokens(settings["machine_end_gcode"], extruder_0_settings)
|
||||
# Use values from the first used extruder by default so we get the expected temperatures
|
||||
initial_extruder_stack = Application.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
|
||||
initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value")
|
||||
|
||||
settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], initial_extruder_nr)
|
||||
settings["machine_end_gcode"] = self._expandGcodeTokens(settings["machine_end_gcode"], initial_extruder_nr)
|
||||
|
||||
# Add all sub-messages for each individual setting.
|
||||
for key, value in settings.items():
|
||||
|
|
|
@ -17,7 +17,7 @@ catalog = i18nCatalog("cura")
|
|||
from cura import LayerDataBuilder
|
||||
from cura import LayerDataDecorator
|
||||
from cura.LayerPolygon import LayerPolygon
|
||||
from cura.GCodeListDecorator import GCodeListDecorator
|
||||
from cura.Scene.GCodeListDecorator import GCodeListDecorator
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
import numpy
|
||||
|
|
|
@ -59,8 +59,9 @@ class GCodeWriter(MeshWriter):
|
|||
Logger.log("e", "GCode Writer does not support non-text mode.")
|
||||
return False
|
||||
|
||||
active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
|
||||
scene = Application.getInstance().getController().getScene()
|
||||
gcode_list = getattr(scene, "gcode_list")
|
||||
gcode_list = getattr(scene, "gcode_list")[active_build_plate]
|
||||
if gcode_list:
|
||||
for gcode in gcode_list:
|
||||
stream.write(gcode)
|
||||
|
|
|
@ -8,12 +8,13 @@ from PyQt5.QtCore import Qt
|
|||
|
||||
from UM.Mesh.MeshReader import MeshReader
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
from .ImageReaderUI import ImageReaderUI
|
||||
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode
|
||||
|
||||
|
||||
class ImageReader(MeshReader):
|
||||
def __init__(self):
|
||||
|
|
|
@ -27,7 +27,9 @@ class MachineSettingsAction(MachineAction):
|
|||
self._qml_url = "MachineSettingsAction.qml"
|
||||
|
||||
self._global_container_stack = None
|
||||
self._container_index = 0
|
||||
|
||||
from cura.Settings.CuraContainerStack import _ContainerIndexes
|
||||
self._container_index = _ContainerIndexes.DefinitionChanges
|
||||
|
||||
self._container_registry = ContainerRegistry.getInstance()
|
||||
self._container_registry.containerAdded.connect(self._onContainerAdded)
|
||||
|
@ -241,6 +243,7 @@ class MachineSettingsAction(MachineAction):
|
|||
"type": "material",
|
||||
"approximate_diameter": machine_approximate_diameter,
|
||||
"material": old_material.getMetaDataEntry("material", "value"),
|
||||
"brand": old_material.getMetaDataEntry("brand", "value"),
|
||||
"supplier": old_material.getMetaDataEntry("supplier", "value"),
|
||||
"color_name": old_material.getMetaDataEntry("color_name", "value"),
|
||||
"definition": materials_definition
|
||||
|
@ -251,6 +254,7 @@ class MachineSettingsAction(MachineAction):
|
|||
if old_material == self._empty_container:
|
||||
search_criteria.pop("material", None)
|
||||
search_criteria.pop("supplier", None)
|
||||
search_criteria.pop("brand", None)
|
||||
search_criteria.pop("definition", None)
|
||||
search_criteria["id"] = extruder_stack.getMetaDataEntry("preferred_material")
|
||||
|
||||
|
@ -258,6 +262,7 @@ class MachineSettingsAction(MachineAction):
|
|||
if not materials:
|
||||
# Same material with new diameter is not found, search for generic version of the same material type
|
||||
search_criteria.pop("supplier", None)
|
||||
search_criteria.pop("brand", None)
|
||||
search_criteria["color_name"] = "Generic"
|
||||
materials = self._container_registry.findInstanceContainers(**search_criteria)
|
||||
if not materials:
|
||||
|
@ -274,6 +279,6 @@ class MachineSettingsAction(MachineAction):
|
|||
# Just use empty material as a final fallback
|
||||
materials = [self._empty_container]
|
||||
|
||||
Logger.log("i", "Selecting new material: %s" % materials[0].getId())
|
||||
Logger.log("i", "Selecting new material: %s", materials[0].getId())
|
||||
|
||||
extruder_stack.material = materials[0]
|
||||
|
|
|
@ -393,7 +393,7 @@ Cura.MachineAction
|
|||
property string label: catalog.i18nc("@label", "Material diameter")
|
||||
property string unit: catalog.i18nc("@label", "mm")
|
||||
property string tooltip: catalog.i18nc("@tooltip", "The nominal diameter of filament supported by the printer. The exact diameter will be overridden by the material and/or the profile.")
|
||||
property var afterOnEditingFinished:
|
||||
function afterOnEditingFinished()
|
||||
{
|
||||
if (settingsTabs.currentIndex > 0)
|
||||
{
|
||||
|
|
|
@ -111,7 +111,7 @@ Item {
|
|||
ScrollView
|
||||
{
|
||||
height: parent.height
|
||||
width: UM.Theme.getSize("setting").width
|
||||
width: UM.Theme.getSize("setting").width + UM.Theme.getSize("default_margin").width
|
||||
style: UM.Theme.styles.scrollview
|
||||
|
||||
ListView
|
||||
|
|
|
@ -93,6 +93,7 @@ class SimulationPass(RenderPass):
|
|||
self.bind()
|
||||
|
||||
tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Overlay, backface_cull = True)
|
||||
active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
|
||||
head_position = None # Indicates the current position of the print head
|
||||
nozzle_node = None
|
||||
|
||||
|
@ -105,7 +106,7 @@ class SimulationPass(RenderPass):
|
|||
nozzle_node = node
|
||||
nozzle_node.setVisible(False)
|
||||
|
||||
elif isinstance(node, SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible():
|
||||
elif issubclass(type(node), SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible() and node.callDecoration("getBuildPlateNumber") == active_build_plate:
|
||||
layer_data = node.callDecoration("getLayerData")
|
||||
if not layer_data:
|
||||
continue
|
||||
|
|
|
@ -25,7 +25,7 @@ from UM.View.GL.OpenGL import OpenGL
|
|||
from UM.View.GL.OpenGLContext import OpenGLContext
|
||||
from UM.View.View import View
|
||||
from UM.i18n import i18nCatalog
|
||||
from cura.ConvexHullNode import ConvexHullNode
|
||||
from cura.Scene.ConvexHullNode import ConvexHullNode
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
from .NozzleNode import NozzleNode
|
||||
|
|
|
@ -145,35 +145,42 @@ geometry41core =
|
|||
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head + g_vertex_offset_vert));
|
||||
//And reverse so that the line is also visible from the back side.
|
||||
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
|
||||
|
||||
EndPrimitive();
|
||||
} else {
|
||||
// All normal lines are rendered as 3d tubes.
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz));
|
||||
|
||||
EndPrimitive();
|
||||
|
||||
// left side
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head));
|
||||
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
|
||||
|
||||
EndPrimitive();
|
||||
|
||||
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
|
||||
|
||||
EndPrimitive();
|
||||
|
||||
|
|
|
@ -166,7 +166,7 @@ class SliceInfo(Extension):
|
|||
|
||||
data["models"].append(model)
|
||||
|
||||
print_times = print_information._print_time_message_values
|
||||
print_times = print_information.printTimes()
|
||||
data["print_times"] = {"travel": int(print_times["travel"].getDisplayString(DurationFormat.Format.Seconds)),
|
||||
"support": int(print_times["support"].getDisplayString(DurationFormat.Format.Seconds)),
|
||||
"infill": int(print_times["infill"].getDisplayString(DurationFormat.Format.Seconds)),
|
||||
|
|
|
@ -11,7 +11,7 @@ from UM.Math.Matrix import Matrix
|
|||
from UM.Math.Vector import Vector
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
from UM.Mesh.MeshReader import MeshReader
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode
|
||||
|
||||
MYPY = False
|
||||
try:
|
||||
|
@ -19,63 +19,63 @@ try:
|
|||
import xml.etree.cElementTree as ET
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
# TODO: preserve the structure of scenes that contain several objects
|
||||
# Use CADPart, for example, to distinguish between separate objects
|
||||
|
||||
# Use CADPart, for example, to distinguish between separate objects
|
||||
|
||||
DEFAULT_SUBDIV = 16 # Default subdivision factor for spheres, cones, and cylinders
|
||||
EPSILON = 0.000001
|
||||
|
||||
class Shape:
|
||||
|
||||
|
||||
# Expects verts in MeshBuilder-ready format, as a n by 3 mdarray
|
||||
# with vertices stored in rows
|
||||
def __init__(self, verts, faces, index_base, name):
|
||||
self.verts = verts
|
||||
self.faces = faces
|
||||
# Those are here for debugging purposes only
|
||||
self.index_base = index_base
|
||||
self.index_base = index_base
|
||||
self.name = name
|
||||
|
||||
|
||||
class X3DReader(MeshReader):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._supported_extensions = [".x3d"]
|
||||
self._namespaces = {}
|
||||
|
||||
|
||||
# Main entry point
|
||||
# Reads the file, returns a SceneNode (possibly with nested ones), or None
|
||||
def read(self, file_name):
|
||||
try:
|
||||
self.defs = {}
|
||||
self.shapes = []
|
||||
|
||||
|
||||
tree = ET.parse(file_name)
|
||||
xml_root = tree.getroot()
|
||||
|
||||
|
||||
if xml_root.tag != "X3D":
|
||||
return None
|
||||
|
||||
scale = 1000 # Default X3D unit it one meter, while Cura's is one millimeters
|
||||
scale = 1000 # Default X3D unit it one meter, while Cura's is one millimeters
|
||||
if xml_root[0].tag == "head":
|
||||
for head_node in xml_root[0]:
|
||||
if head_node.tag == "unit" and head_node.attrib.get("category") == "length":
|
||||
scale *= float(head_node.attrib["conversionFactor"])
|
||||
break
|
||||
break
|
||||
xml_scene = xml_root[1]
|
||||
else:
|
||||
xml_scene = xml_root[0]
|
||||
|
||||
|
||||
if xml_scene.tag != "Scene":
|
||||
return None
|
||||
|
||||
|
||||
self.transform = Matrix()
|
||||
self.transform.setByScaleFactor(scale)
|
||||
self.index_base = 0
|
||||
|
||||
|
||||
# Traverse the scene tree, populate the shapes list
|
||||
self.processChildNodes(xml_scene)
|
||||
|
||||
|
||||
if self.shapes:
|
||||
builder = MeshBuilder()
|
||||
builder.setVertices(numpy.concatenate([shape.verts for shape in self.shapes]))
|
||||
|
@ -95,20 +95,20 @@ class X3DReader(MeshReader):
|
|||
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
except Exception:
|
||||
Logger.logException("e", "Exception in X3D reader")
|
||||
return None
|
||||
|
||||
return node
|
||||
|
||||
|
||||
# ------------------------- XML tree traversal
|
||||
|
||||
|
||||
def processNode(self, xml_node):
|
||||
xml_node = self.resolveDefUse(xml_node)
|
||||
if xml_node is None:
|
||||
return
|
||||
|
||||
|
||||
tag = xml_node.tag
|
||||
if tag in ("Group", "StaticGroup", "CADAssembly", "CADFace", "CADLayer", "Collision"):
|
||||
self.processChildNodes(xml_node)
|
||||
|
@ -120,8 +120,8 @@ class X3DReader(MeshReader):
|
|||
self.processTransform(xml_node)
|
||||
elif tag == "Shape":
|
||||
self.processShape(xml_node)
|
||||
|
||||
|
||||
|
||||
|
||||
def processShape(self, xml_node):
|
||||
# Find the geometry and the appearance inside the Shape
|
||||
geometry = appearance = None
|
||||
|
@ -130,21 +130,21 @@ class X3DReader(MeshReader):
|
|||
appearance = self.resolveDefUse(sub_node)
|
||||
elif sub_node.tag in self.geometry_importers and not geometry:
|
||||
geometry = self.resolveDefUse(sub_node)
|
||||
|
||||
# TODO: appearance is completely ignored. At least apply the material color...
|
||||
|
||||
# TODO: appearance is completely ignored. At least apply the material color...
|
||||
if not geometry is None:
|
||||
try:
|
||||
self.verts = self.faces = [] # Safeguard
|
||||
self.verts = self.faces = [] # Safeguard
|
||||
self.geometry_importers[geometry.tag](self, geometry)
|
||||
m = self.transform.getData()
|
||||
verts = m.dot(self.verts)[:3].transpose()
|
||||
|
||||
|
||||
self.shapes.append(Shape(verts, self.faces, self.index_base, geometry.tag))
|
||||
self.index_base += len(verts)
|
||||
|
||||
|
||||
except Exception:
|
||||
Logger.logException("e", "Exception in X3D reader while reading %s", geometry.tag)
|
||||
|
||||
|
||||
# Returns the referenced node if the node has USE, the same node otherwise.
|
||||
# May return None is USE points at a nonexistent node
|
||||
# In X3DOM, when both DEF and USE are in the same node, DEF is ignored.
|
||||
|
@ -155,34 +155,34 @@ class X3DReader(MeshReader):
|
|||
if USE:
|
||||
return self.defs.get(USE, None)
|
||||
|
||||
DEF = node.attrib.get("DEF")
|
||||
DEF = node.attrib.get("DEF")
|
||||
if DEF:
|
||||
self.defs[DEF] = node
|
||||
self.defs[DEF] = node
|
||||
return node
|
||||
|
||||
|
||||
def processChildNodes(self, node):
|
||||
for c in node:
|
||||
self.processNode(c)
|
||||
Job.yieldThread()
|
||||
|
||||
|
||||
# Since this is a grouping node, will recurse down the tree.
|
||||
# According to the spec, the final transform matrix is:
|
||||
# T * C * R * SR * S * -SR * -C
|
||||
# Where SR corresponds to the rotation matrix to scaleOrientation
|
||||
# C and SR are rather exotic. S, slightly less so.
|
||||
# C and SR are rather exotic. S, slightly less so.
|
||||
def processTransform(self, node):
|
||||
rot = readRotation(node, "rotation", (0, 0, 1, 0)) # (angle, axisVactor) tuple
|
||||
trans = readVector(node, "translation", (0, 0, 0)) # Vector
|
||||
scale = readVector(node, "scale", (1, 1, 1)) # Vector
|
||||
center = readVector(node, "center", (0, 0, 0)) # Vector
|
||||
scale_orient = readRotation(node, "scaleOrientation", (0, 0, 1, 0)) # (angle, axisVactor) tuple
|
||||
|
||||
# Store the previous transform; in Cura, the default matrix multiplication is in place
|
||||
|
||||
# Store the previous transform; in Cura, the default matrix multiplication is in place
|
||||
prev = Matrix(self.transform.getData()) # It's deep copy, I've checked
|
||||
|
||||
|
||||
# The rest of transform manipulation will be applied in place
|
||||
got_center = (center.x != 0 or center.y != 0 or center.z != 0)
|
||||
|
||||
|
||||
T = self.transform
|
||||
if trans.x != 0 or trans.y != 0 or trans.z != 0:
|
||||
T.translate(trans)
|
||||
|
@ -202,13 +202,13 @@ class X3DReader(MeshReader):
|
|||
T.rotateByAxis(-scale_orient[0], scale_orient[1])
|
||||
if got_center:
|
||||
T.translate(-center)
|
||||
|
||||
|
||||
self.processChildNodes(node)
|
||||
self.transform = prev
|
||||
|
||||
|
||||
# ------------------------- Geometry importers
|
||||
# They are supposed to fill the self.verts and self.faces arrays, the caller will do the rest
|
||||
|
||||
|
||||
# Primitives
|
||||
|
||||
def processGeometryBox(self, node):
|
||||
|
@ -228,14 +228,14 @@ class X3DReader(MeshReader):
|
|||
self.addVertex(-dx, -dy, dz)
|
||||
self.addVertex(-dx, -dy, -dz)
|
||||
self.addVertex(dx, -dy, -dz)
|
||||
|
||||
|
||||
self.addQuad(0, 1, 2, 3) # +y
|
||||
self.addQuad(4, 0, 3, 7) # +x
|
||||
self.addQuad(7, 3, 2, 6) # -z
|
||||
self.addQuad(6, 2, 1, 5) # -x
|
||||
self.addQuad(5, 1, 0, 4) # +z
|
||||
self.addQuad(7, 6, 5, 4) # -y
|
||||
|
||||
|
||||
# The sphere is subdivided into nr rings and ns segments
|
||||
def processGeometrySphere(self, node):
|
||||
r = readFloat(node, "radius", 0.5)
|
||||
|
@ -247,16 +247,16 @@ class X3DReader(MeshReader):
|
|||
(nr, ns) = subdiv
|
||||
else:
|
||||
nr = ns = DEFAULT_SUBDIV
|
||||
|
||||
|
||||
lau = pi / nr # Unit angle of latitude (rings) for the given tesselation
|
||||
lou = 2 * pi / ns # Unit angle of longitude (segments)
|
||||
|
||||
|
||||
self.reserveFaceAndVertexCount(ns*(nr*2 - 2), 2 + (nr - 1)*ns)
|
||||
|
||||
|
||||
# +y and -y poles
|
||||
self.addVertex(0, r, 0)
|
||||
self.addVertex(0, -r, 0)
|
||||
|
||||
|
||||
# The non-polar vertices go from x=0, negative z plane counterclockwise -
|
||||
# to -x, to +z, to +x, back to -z
|
||||
for ring in range(1, nr):
|
||||
|
@ -264,12 +264,12 @@ class X3DReader(MeshReader):
|
|||
self.addVertex(-r*sin(lou * seg) * sin(lau * ring),
|
||||
r*cos(lau * ring),
|
||||
-r*cos(lou * seg) * sin(lau * ring))
|
||||
|
||||
|
||||
vb = 2 + (nr - 2) * ns # First vertex index for the bottom cap
|
||||
|
||||
|
||||
# Faces go in order: top cap, sides, bottom cap.
|
||||
# Sides go by ring then by segment.
|
||||
|
||||
|
||||
# Caps
|
||||
# Top cap face vertices go in order: down right up
|
||||
# (starting from +y pole)
|
||||
|
@ -277,7 +277,7 @@ class X3DReader(MeshReader):
|
|||
for seg in range(ns):
|
||||
self.addTri(0, seg + 2, (seg + 1) % ns + 2)
|
||||
self.addTri(1, vb + (seg + 1) % ns, vb + seg)
|
||||
|
||||
|
||||
# Sides
|
||||
# Side face vertices go in order: down right upleft, downright up left
|
||||
for ring in range(nr - 2):
|
||||
|
@ -288,24 +288,24 @@ class X3DReader(MeshReader):
|
|||
for seg in range(ns):
|
||||
nseg = (seg + 1) % ns
|
||||
self.addQuad(tvb + seg, bvb + seg, bvb + nseg, tvb + nseg)
|
||||
|
||||
|
||||
def processGeometryCone(self, node):
|
||||
r = readFloat(node, "bottomRadius", 1)
|
||||
height = readFloat(node, "height", 2)
|
||||
bottom = readBoolean(node, "bottom", True)
|
||||
side = readBoolean(node, "side", True)
|
||||
n = readInt(node, "subdivision", DEFAULT_SUBDIV)
|
||||
|
||||
|
||||
d = height / 2
|
||||
angle = 2 * pi / n
|
||||
|
||||
|
||||
self.reserveFaceAndVertexCount((n if side else 0) + (n-2 if bottom else 0), n+1)
|
||||
|
||||
|
||||
# Vertex 0 is the apex, vertices 1..n are the bottom
|
||||
self.addVertex(0, d, 0)
|
||||
for i in range(n):
|
||||
self.addVertex(-r * sin(angle * i), -d, -r * cos(angle * i))
|
||||
|
||||
|
||||
# Side face vertices go: up down right
|
||||
if side:
|
||||
for i in range(n):
|
||||
|
@ -313,7 +313,7 @@ class X3DReader(MeshReader):
|
|||
if bottom:
|
||||
for i in range(2, n):
|
||||
self.addTri(1, i, i+1)
|
||||
|
||||
|
||||
def processGeometryCylinder(self, node):
|
||||
r = readFloat(node, "radius", 1)
|
||||
height = readFloat(node, "height", 2)
|
||||
|
@ -321,13 +321,13 @@ class X3DReader(MeshReader):
|
|||
side = readBoolean(node, "side", True)
|
||||
top = readBoolean(node, "top", True)
|
||||
n = readInt(node, "subdivision", DEFAULT_SUBDIV)
|
||||
|
||||
|
||||
nn = n * 2
|
||||
angle = 2 * pi / n
|
||||
hh = height/2
|
||||
|
||||
|
||||
self.reserveFaceAndVertexCount((nn if side else 0) + (n - 2 if top else 0) + (n - 2 if bottom else 0), nn)
|
||||
|
||||
|
||||
# The seam is at x=0, z=-r, vertices go ccw -
|
||||
# to pos x, to neg z, to neg x, back to neg z
|
||||
for i in range(n):
|
||||
|
@ -335,18 +335,18 @@ class X3DReader(MeshReader):
|
|||
rc = -r * cos(angle * i)
|
||||
self.addVertex(rs, hh, rc)
|
||||
self.addVertex(rs, -hh, rc)
|
||||
|
||||
|
||||
if side:
|
||||
for i in range(n):
|
||||
ni = (i + 1) % n
|
||||
self.addQuad(ni * 2 + 1, ni * 2, i * 2, i * 2 + 1)
|
||||
|
||||
|
||||
for i in range(2, nn-3, 2):
|
||||
if top:
|
||||
self.addTri(0, i, i+2)
|
||||
if bottom:
|
||||
self.addTri(1, i+1, i+3)
|
||||
|
||||
|
||||
# Semi-primitives
|
||||
|
||||
def processGeometryElevationGrid(self, node):
|
||||
|
@ -356,21 +356,21 @@ class X3DReader(MeshReader):
|
|||
nz = readInt(node, "zDimension", 0)
|
||||
height = readFloatArray(node, "height", False)
|
||||
ccw = readBoolean(node, "ccw", True)
|
||||
|
||||
|
||||
if nx <= 0 or nz <= 0 or len(height) < nx*nz:
|
||||
return # That's weird, the wording of the standard suggests grids with zero quads are somehow valid
|
||||
|
||||
|
||||
self.reserveFaceAndVertexCount(2*(nx-1)*(nz-1), nx*nz)
|
||||
|
||||
|
||||
for z in range(nz):
|
||||
for x in range(nx):
|
||||
self.addVertex(x * dx, height[z*nx + x], z * dz)
|
||||
|
||||
|
||||
for z in range(1, nz):
|
||||
for x in range(1, nx):
|
||||
self.addTriFlip((z - 1)*nx + x - 1, z*nx + x, (z - 1)*nx + x, ccw)
|
||||
self.addTriFlip((z - 1)*nx + x - 1, z*nx + x - 1, z*nx + x, ccw)
|
||||
|
||||
|
||||
def processGeometryExtrusion(self, node):
|
||||
ccw = readBoolean(node, "ccw", True)
|
||||
begin_cap = readBoolean(node, "beginCap", True)
|
||||
|
@ -384,23 +384,23 @@ class X3DReader(MeshReader):
|
|||
# This converts X3D's axis/angle rotation to a 3x3 numpy matrix
|
||||
def toRotationMatrix(rot):
|
||||
(x, y, z) = rot[:3]
|
||||
a = rot[3]
|
||||
a = rot[3]
|
||||
s = sin(a)
|
||||
c = cos(a)
|
||||
t = 1-c
|
||||
return numpy.array((
|
||||
(x * x * t + c, x * y * t - z*s, x * z * t + y * s),
|
||||
(x * y * t + z*s, y * y * t + c, y * z * t - x * s),
|
||||
(x * z * t - y * s, y * z * t + x * s, z * z * t + c)))
|
||||
|
||||
(x * z * t - y * s, y * z * t + x * s, z * z * t + c)))
|
||||
|
||||
orient = [toRotationMatrix(orient[i:i+4]) if orient[i+3] != 0 else None for i in range(0, len(orient), 4)]
|
||||
|
||||
|
||||
scale = readFloatArray(node, "scale", None)
|
||||
if scale:
|
||||
scale = [numpy.array(((scale[i], 0, 0), (0, 1, 0), (0, 0, scale[i+1])))
|
||||
if scale[i] != 1 or scale[i+1] != 1 else None for i in range(0, len(scale), 2)]
|
||||
|
||||
|
||||
|
||||
|
||||
# Special treatment for the closed spine and cross section.
|
||||
# Let's save some memory by not creating identical but distinct vertices;
|
||||
# later we'll introduce conditional logic to link the last vertex with
|
||||
|
@ -413,14 +413,14 @@ class X3DReader(MeshReader):
|
|||
ncf = nc if crossClosed else nc - 1
|
||||
# Face count along the cross; for closed cross, it's the same as the
|
||||
# respective vertex count
|
||||
|
||||
|
||||
spine_closed = spine[0] == spine[-1]
|
||||
if spine_closed:
|
||||
spine = spine[:-1]
|
||||
ns = len(spine)
|
||||
spine = [Vector(*s) for s in spine]
|
||||
nsf = ns if spine_closed else ns - 1
|
||||
|
||||
|
||||
# This will be used for fallback, where the current spine point joins
|
||||
# two collinear spine segments. No need to recheck the case of the
|
||||
# closed spine/last-to-first point juncture; if there's an angle there,
|
||||
|
@ -442,7 +442,7 @@ class X3DReader(MeshReader):
|
|||
if v.cross(orig_y).length() > EPSILON:
|
||||
# Spine at angle with global y - rotate the z accordingly
|
||||
a = v.cross(orig_y) # Axis of rotation to get to the Z
|
||||
(x, y, z) = a.normalized().getData()
|
||||
(x, y, z) = a.normalized().getData()
|
||||
s = a.length()/v.length()
|
||||
c = sqrt(1-s*s)
|
||||
t = 1-c
|
||||
|
@ -452,7 +452,7 @@ class X3DReader(MeshReader):
|
|||
(x * z * t + y * s, y * z * t - x * s, z * z * t + c)))
|
||||
orig_z = Vector(*m.dot(orig_z.getData()))
|
||||
return orig_z
|
||||
|
||||
|
||||
self.reserveFaceAndVertexCount(2*nsf*ncf + (nc - 2 if begin_cap else 0) + (nc - 2 if end_cap else 0), ns*nc)
|
||||
|
||||
z = None
|
||||
|
@ -482,151 +482,151 @@ class X3DReader(MeshReader):
|
|||
y = spt - sprev
|
||||
# If there's more than one point in the spine, z is already set.
|
||||
# One point in the spline is an error anyway.
|
||||
|
||||
|
||||
z = z.normalized()
|
||||
y = y.normalized()
|
||||
x = y.cross(z) # Already normalized
|
||||
m = numpy.array(((x.x, y.x, z.x), (x.y, y.y, z.y), (x.z, y.z, z.z)))
|
||||
|
||||
|
||||
# Columns are the unit vectors for the xz plane for the cross-section
|
||||
if orient:
|
||||
mrot = orient[i] if len(orient) > 1 else orient[0]
|
||||
if not mrot is None:
|
||||
m = m.dot(mrot) # Tested against X3DOM, the result matches, still not sure :(
|
||||
|
||||
|
||||
if scale:
|
||||
mscale = scale[i] if len(scale) > 1 else scale[0]
|
||||
if not mscale is None:
|
||||
m = m.dot(mscale)
|
||||
|
||||
|
||||
# First the cross-section 2-vector is scaled,
|
||||
# then rotated (which may make it a 3-vector),
|
||||
# then applied to the xz plane unit vectors
|
||||
|
||||
|
||||
sptv3 = numpy.array(spt.getData()[:3])
|
||||
for cpt in cross:
|
||||
v = sptv3 + m.dot(cpt)
|
||||
self.addVertex(*v)
|
||||
|
||||
|
||||
if begin_cap:
|
||||
self.addFace([x for x in range(nc - 1, -1, -1)], ccw)
|
||||
|
||||
|
||||
# Order of edges in the face: forward along cross, forward along spine,
|
||||
# backward along cross, backward along spine, flipped if now ccw.
|
||||
# This order is assumed later in the texture coordinate assignment;
|
||||
# please don't change without syncing.
|
||||
|
||||
|
||||
for s in range(ns - 1):
|
||||
for c in range(ncf):
|
||||
self.addQuadFlip(s * nc + c, s * nc + (c + 1) % nc,
|
||||
(s + 1) * nc + (c + 1) % nc, (s + 1) * nc + c, ccw)
|
||||
|
||||
|
||||
if spine_closed:
|
||||
# The faces between the last and the first spine points
|
||||
b = (ns - 1) * nc
|
||||
for c in range(ncf):
|
||||
self.addQuadFlip(b + c, b + (c + 1) % nc,
|
||||
(c + 1) % nc, c, ccw)
|
||||
|
||||
|
||||
if end_cap:
|
||||
self.addFace([(ns - 1) * nc + x for x in range(0, nc)], ccw)
|
||||
|
||||
|
||||
# Triangle meshes
|
||||
|
||||
# Helper for numerous nodes with a Coordinate subnode holding vertices
|
||||
# That all triangle meshes and IndexedFaceSet
|
||||
# num_faces can be a function, in case the face count is a function of vertex count
|
||||
# num_faces can be a function, in case the face count is a function of vertex count
|
||||
def startCoordMesh(self, node, num_faces):
|
||||
ccw = readBoolean(node, "ccw", True)
|
||||
self.readVertices(node) # This will allocate and fill the vertex array
|
||||
if hasattr(num_faces, "__call__"):
|
||||
num_faces = num_faces(self.getVertexCount())
|
||||
self.reserveFaceCount(num_faces)
|
||||
|
||||
|
||||
return ccw
|
||||
|
||||
|
||||
|
||||
def processGeometryIndexedTriangleSet(self, node):
|
||||
index = readIntArray(node, "index", [])
|
||||
num_faces = len(index) // 3
|
||||
ccw = int(self.startCoordMesh(node, num_faces))
|
||||
|
||||
|
||||
for i in range(0, num_faces*3, 3):
|
||||
self.addTri(index[i + 1 - ccw], index[i + ccw], index[i+2])
|
||||
|
||||
|
||||
def processGeometryIndexedTriangleStripSet(self, node):
|
||||
strips = readIndex(node, "index")
|
||||
ccw = int(self.startCoordMesh(node, sum([len(strip) - 2 for strip in strips])))
|
||||
|
||||
|
||||
for strip in strips:
|
||||
sccw = ccw # Running CCW value, reset for each strip
|
||||
for i in range(len(strip) - 2):
|
||||
self.addTri(strip[i + 1 - sccw], strip[i + sccw], strip[i+2])
|
||||
sccw = 1 - sccw
|
||||
|
||||
|
||||
def processGeometryIndexedTriangleFanSet(self, node):
|
||||
fans = readIndex(node, "index")
|
||||
ccw = int(self.startCoordMesh(node, sum([len(fan) - 2 for fan in fans])))
|
||||
|
||||
|
||||
for fan in fans:
|
||||
for i in range(1, len(fan) - 1):
|
||||
self.addTri(fan[0], fan[i + 1 - ccw], fan[i + ccw])
|
||||
|
||||
|
||||
def processGeometryTriangleSet(self, node):
|
||||
ccw = int(self.startCoordMesh(node, lambda num_vert: num_vert // 3))
|
||||
for i in range(0, self.getVertexCount(), 3):
|
||||
self.addTri(i + 1 - ccw, i + ccw, i+2)
|
||||
|
||||
|
||||
def processGeometryTriangleStripSet(self, node):
|
||||
strips = readIntArray(node, "stripCount", [])
|
||||
ccw = int(self.startCoordMesh(node, sum([n-2 for n in strips])))
|
||||
|
||||
|
||||
vb = 0
|
||||
for n in strips:
|
||||
sccw = ccw
|
||||
for i in range(n-2):
|
||||
for i in range(n-2):
|
||||
self.addTri(vb + i + 1 - sccw, vb + i + sccw, vb + i + 2)
|
||||
sccw = 1 - sccw
|
||||
vb += n
|
||||
|
||||
|
||||
def processGeometryTriangleFanSet(self, node):
|
||||
fans = readIntArray(node, "fanCount", [])
|
||||
ccw = int(self.startCoordMesh(node, sum([n-2 for n in fans])))
|
||||
|
||||
|
||||
vb = 0
|
||||
for n in fans:
|
||||
for i in range(1, n-1):
|
||||
for i in range(1, n-1):
|
||||
self.addTri(vb, vb + i + 1 - ccw, vb + i + ccw)
|
||||
vb += n
|
||||
|
||||
|
||||
# Quad geometries from the CAD module, might be relevant for printing
|
||||
|
||||
|
||||
def processGeometryQuadSet(self, node):
|
||||
ccw = self.startCoordMesh(node, lambda num_vert: 2*(num_vert // 4))
|
||||
for i in range(0, self.getVertexCount(), 4):
|
||||
self.addQuadFlip(i, i+1, i+2, i+3, ccw)
|
||||
|
||||
|
||||
def processGeometryIndexedQuadSet(self, node):
|
||||
index = readIntArray(node, "index", [])
|
||||
num_quads = len(index) // 4
|
||||
ccw = self.startCoordMesh(node, num_quads*2)
|
||||
|
||||
|
||||
for i in range(0, num_quads*4, 4):
|
||||
self.addQuadFlip(index[i], index[i+1], index[i+2], index[i+3], ccw)
|
||||
|
||||
|
||||
# 2D polygon geometries
|
||||
# Won't work for now, since Cura expects every mesh to have a nontrivial convex hull
|
||||
# The only way around that is merging meshes.
|
||||
|
||||
|
||||
def processGeometryDisk2D(self, node):
|
||||
innerRadius = readFloat(node, "innerRadius", 0)
|
||||
outerRadius = readFloat(node, "outerRadius", 1)
|
||||
n = readInt(node, "subdivision", DEFAULT_SUBDIV)
|
||||
|
||||
|
||||
angle = 2 * pi / n
|
||||
|
||||
|
||||
self.reserveFaceAndVertexCount(n*4 if innerRadius else n-2, n*2 if innerRadius else n)
|
||||
|
||||
|
||||
for i in range(n):
|
||||
s = sin(angle * i)
|
||||
c = cos(angle * i)
|
||||
|
@ -635,11 +635,11 @@ class X3DReader(MeshReader):
|
|||
self.addVertex(innerRadius*c, innerRadius*s, 0)
|
||||
ni = (i+1) % n
|
||||
self.addQuad(2*i, 2*ni, 2*ni+1, 2*i+1)
|
||||
|
||||
|
||||
if not innerRadius:
|
||||
for i in range(2, n):
|
||||
self.addTri(0, i-1, i)
|
||||
|
||||
|
||||
def processGeometryRectangle2D(self, node):
|
||||
(x, y) = readFloatArray(node, "size", (2, 2))
|
||||
self.reserveFaceAndVertexCount(2, 4)
|
||||
|
@ -648,7 +648,7 @@ class X3DReader(MeshReader):
|
|||
self.addVertex(x/2, y/2, 0)
|
||||
self.addVertex(-x/2, y/2, 0)
|
||||
self.addQuad(0, 1, 2, 3)
|
||||
|
||||
|
||||
def processGeometryTriangleSet2D(self, node):
|
||||
verts = readFloatArray(node, "vertices", ())
|
||||
num_faces = len(verts) // 6;
|
||||
|
@ -656,25 +656,25 @@ class X3DReader(MeshReader):
|
|||
self.reserveFaceAndVertexCount(num_faces, num_faces * 3)
|
||||
for vert in verts:
|
||||
self.addVertex(*vert)
|
||||
|
||||
|
||||
# The front face is on the +Z side, so CCW is a variable
|
||||
for i in range(0, num_faces*3, 3):
|
||||
a = Vector(*verts[i+2]) - Vector(*verts[i])
|
||||
b = Vector(*verts[i+1]) - Vector(*verts[i])
|
||||
self.addTriFlip(i, i+1, i+2, a.x*b.y > a.y*b.x)
|
||||
|
||||
|
||||
# General purpose polygon mesh
|
||||
|
||||
def processGeometryIndexedFaceSet(self, node):
|
||||
faces = readIndex(node, "coordIndex")
|
||||
ccw = self.startCoordMesh(node, sum([len(face) - 2 for face in faces]))
|
||||
|
||||
|
||||
for face in faces:
|
||||
if len(face) == 3:
|
||||
self.addTriFlip(face[0], face[1], face[2], ccw)
|
||||
elif len(face) > 3:
|
||||
self.addFace(face, ccw)
|
||||
|
||||
|
||||
geometry_importers = {
|
||||
"IndexedFaceSet": processGeometryIndexedFaceSet,
|
||||
"IndexedTriangleSet": processGeometryIndexedTriangleSet,
|
||||
|
@ -695,7 +695,7 @@ class X3DReader(MeshReader):
|
|||
"Cylinder": processGeometryCylinder,
|
||||
"Cone": processGeometryCone
|
||||
}
|
||||
|
||||
|
||||
# Parses the Coordinate.@point field, fills the verts array.
|
||||
def readVertices(self, node):
|
||||
for c in node:
|
||||
|
@ -704,9 +704,9 @@ class X3DReader(MeshReader):
|
|||
if not c is None:
|
||||
pt = c.attrib.get("point")
|
||||
if pt:
|
||||
# allow the list of float values in 'point' attribute to
|
||||
# be separated by commas or whitespace as per spec of
|
||||
# XML encoding of X3D
|
||||
# allow the list of float values in 'point' attribute to
|
||||
# be separated by commas or whitespace as per spec of
|
||||
# XML encoding of X3D
|
||||
# Ref ISO/IEC 19776-1:2015 : Section 5.1.2
|
||||
co = [float(x) for vec in pt.split(',') for x in vec.split()]
|
||||
num_verts = len(co) // 3
|
||||
|
@ -715,57 +715,57 @@ class X3DReader(MeshReader):
|
|||
# Group by three
|
||||
for i in range(num_verts):
|
||||
self.verts[:3,i] = co[3*i:3*i+3]
|
||||
|
||||
|
||||
# Mesh builder helpers
|
||||
|
||||
|
||||
def reserveFaceAndVertexCount(self, num_faces, num_verts):
|
||||
# Unlike the Cura MeshBuilder, we use 4-vectors stored as columns for easier transform
|
||||
self.verts = numpy.zeros((4, num_verts), dtype=numpy.float32)
|
||||
self.verts[3,:] = numpy.ones((num_verts), dtype=numpy.float32)
|
||||
self.num_verts = 0
|
||||
self.reserveFaceCount(num_faces)
|
||||
|
||||
|
||||
def reserveFaceCount(self, num_faces):
|
||||
self.faces = numpy.zeros((num_faces, 3), dtype=numpy.int32)
|
||||
self.num_faces = 0
|
||||
|
||||
|
||||
def getVertexCount(self):
|
||||
return self.verts.shape[1]
|
||||
|
||||
|
||||
def addVertex(self, x, y, z):
|
||||
self.verts[0, self.num_verts] = x
|
||||
self.verts[1, self.num_verts] = y
|
||||
self.verts[2, self.num_verts] = z
|
||||
self.num_verts += 1
|
||||
|
||||
|
||||
# Indices are 0-based for this shape, but they won't be zero-based in the merged mesh
|
||||
def addTri(self, a, b, c):
|
||||
self.faces[self.num_faces, 0] = self.index_base + a
|
||||
self.faces[self.num_faces, 1] = self.index_base + b
|
||||
self.faces[self.num_faces, 2] = self.index_base + c
|
||||
self.num_faces += 1
|
||||
|
||||
|
||||
def addTriFlip(self, a, b, c, ccw):
|
||||
if ccw:
|
||||
self.addTri(a, b, c)
|
||||
else:
|
||||
self.addTri(b, a, c)
|
||||
|
||||
|
||||
# Needs to be convex, but not necessaily planar
|
||||
# Assumed ccw, cut along the ac diagonal
|
||||
def addQuad(self, a, b, c, d):
|
||||
self.addTri(a, b, c)
|
||||
self.addTri(c, d, a)
|
||||
|
||||
|
||||
def addQuadFlip(self, a, b, c, d, ccw):
|
||||
if ccw:
|
||||
self.addTri(a, b, c)
|
||||
self.addTri(c, d, a)
|
||||
else:
|
||||
self.addTri(a, c, b)
|
||||
self.addTri(c, a, d)
|
||||
|
||||
|
||||
self.addTri(c, a, d)
|
||||
|
||||
|
||||
# Arbitrary polygon triangulation.
|
||||
# Doesn't assume convexity and doesn't check the "convex" flag in the file.
|
||||
# Works by the "cutting of ears" algorithm:
|
||||
|
@ -776,13 +776,13 @@ class X3DReader(MeshReader):
|
|||
def addFace(self, indices, ccw):
|
||||
# Resolve indices to coordinates for faster math
|
||||
face = [Vector(data=self.verts[0:3, i]) for i in indices]
|
||||
|
||||
|
||||
# Need a normal to the plane so that we can know which vertices form inner angles
|
||||
normal = findOuterNormal(face)
|
||||
|
||||
|
||||
if not normal: # Couldn't find an outer edge, non-planar polygon maybe?
|
||||
return
|
||||
|
||||
|
||||
# Find the vertex with the smallest inner angle and no points inside, cut off. Repeat until done
|
||||
n = len(face)
|
||||
vi = [i for i in range(n)] # We'll be using this to kick vertices from the face
|
||||
|
@ -807,17 +807,17 @@ class X3DReader(MeshReader):
|
|||
if pointInsideTriangle(vx, next, prev, nextXprev):
|
||||
no_points_inside = False
|
||||
break
|
||||
|
||||
|
||||
if no_points_inside:
|
||||
max_cos = cos
|
||||
i_min = i
|
||||
|
||||
|
||||
self.addTriFlip(indices[vi[(i_min + n - 1) % n]], indices[vi[i_min]], indices[vi[(i_min + 1) % n]], ccw)
|
||||
vi.pop(i_min)
|
||||
n -= 1
|
||||
self.addTriFlip(indices[vi[0]], indices[vi[1]], indices[vi[2]], ccw)
|
||||
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# X3D field parsers
|
||||
# ------------------------------------------------------------
|
||||
|
@ -844,7 +844,7 @@ def readInt(node, attr, default):
|
|||
if not s:
|
||||
return default
|
||||
return int(s, 0)
|
||||
|
||||
|
||||
def readBoolean(node, attr, default):
|
||||
s = node.attrib.get(attr)
|
||||
if not s:
|
||||
|
@ -873,8 +873,8 @@ def readIndex(node, attr):
|
|||
chunk.append(v[i])
|
||||
if chunk:
|
||||
chunks.append(chunk)
|
||||
return chunks
|
||||
|
||||
return chunks
|
||||
|
||||
# Given a face as a sequence of vectors, returns a normal to the polygon place that forms a right triple
|
||||
# with a vector along the polygon sequence and a vector backwards
|
||||
def findOuterNormal(face):
|
||||
|
@ -894,25 +894,25 @@ def findOuterNormal(face):
|
|||
if rejection.dot(prev_rejection) < -EPSILON: # points on both sides of the edge - not an outer one
|
||||
is_outer = False
|
||||
break
|
||||
elif rejection.length() > prev_rejection.length(): # Pick a greater rejection for numeric stability
|
||||
elif rejection.length() > prev_rejection.length(): # Pick a greater rejection for numeric stability
|
||||
prev_rejection = rejection
|
||||
|
||||
|
||||
if is_outer: # Found an outer edge, prev_rejection is the rejection inside the face. Generate a normal.
|
||||
return edge.cross(prev_rejection)
|
||||
|
||||
return False
|
||||
|
||||
# Given two *collinear* vectors a and b, returns the coefficient that takes b to a.
|
||||
# Given two *collinear* vectors a and b, returns the coefficient that takes b to a.
|
||||
# No error handling.
|
||||
# For stability, taking the ration between the biggest coordinates would be better...
|
||||
# For stability, taking the ration between the biggest coordinates would be better...
|
||||
def ratio(a, b):
|
||||
if b.x > EPSILON or b.x < -EPSILON:
|
||||
return a.x / b.x
|
||||
elif b.y > EPSILON or b.y < -EPSILON:
|
||||
return a.y / b.y
|
||||
else:
|
||||
return a.z / b.z
|
||||
|
||||
return a.z / b.z
|
||||
|
||||
def pointInsideTriangle(vx, next, prev, nextXprev):
|
||||
vxXprev = vx.cross(prev)
|
||||
r = ratio(vxXprev, nextXprev)
|
||||
|
@ -921,4 +921,4 @@ def pointInsideTriangle(vx, next, prev, nextXprev):
|
|||
vxXnext = vx.cross(next);
|
||||
s = -ratio(vxXnext, nextXprev)
|
||||
return s > 0 and (s + r) < 1
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue