Proof of concept for simulation

Co-authored-by: Casper Lamboo <c.lamboo@ultimaker.com>

CURA-7647
This commit is contained in:
saumya.jain 2023-12-19 10:12:56 +01:00
parent 3c550557b9
commit cfec5e0cc1
5 changed files with 128 additions and 104 deletions

View file

@ -67,7 +67,7 @@ class LayerPolygon:
# Buffering the colors shouldn't be necessary as it is not # Buffering the colors shouldn't be necessary as it is not
# re-used and can save a lot of memory usage. # re-used and can save a lot of memory usage.
self._color_map = LayerPolygon.getColorMap() self._color_map = LayerPolygon.getColorMap()
self._colors = self._color_map[self._types] # type: numpy.ndarray self._colors: numpy.ndarray = self._color_map[self._types]
# When type is used as index returns true if type == LayerPolygon.InfillType # When type is used as index returns true if type == LayerPolygon.InfillType
# or type == LayerPolygon.SkinType # or type == LayerPolygon.SkinType
@ -75,8 +75,8 @@ class LayerPolygon:
# Should be generated in better way, not hardcoded. # Should be generated in better way, not hardcoded.
self._is_infill_or_skin_type_map = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], dtype=bool) self._is_infill_or_skin_type_map = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], dtype=bool)
self._build_cache_line_mesh_mask = None # type: Optional[numpy.ndarray] self._build_cache_line_mesh_mask: Optional[numpy.ndarray] = None
self._build_cache_needed_points = None # type: Optional[numpy.ndarray] self._build_cache_needed_points: Optional[numpy.ndarray] = None
def buildCache(self) -> None: def buildCache(self) -> None:
# For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out. # For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out.

View file

@ -35,7 +35,7 @@ class SimulationPass(RenderPass):
self._nozzle_shader = None self._nozzle_shader = None
self._disabled_shader = None self._disabled_shader = None
self._old_current_layer = 0 self._old_current_layer = 0
self._old_current_path = 0 self._old_current_path: float = 0.0
self._switching_layers = True # Tracking whether the user is moving across layers (True) or across paths (False). If false, lower layers render as shadowy. self._switching_layers = True # Tracking whether the user is moving across layers (True) or across paths (False). If false, lower layers render as shadowy.
self._gl = OpenGL.getInstance().getBindingsObject() self._gl = OpenGL.getInstance().getBindingsObject()
self._scene = Application.getInstance().getController().getScene() self._scene = Application.getInstance().getController().getScene()
@ -139,7 +139,7 @@ class SimulationPass(RenderPass):
continue continue
# Render all layers below a certain number as line mesh instead of vertices. # Render all layers below a certain number as line mesh instead of vertices.
if self._layer_view._current_layer_num > -1 and ((not self._layer_view._only_show_top_layers) or (not self._layer_view.getCompatibilityMode())): if self._layer_view.getCurrentLayer() > -1 and ((not self._layer_view._only_show_top_layers) or (not self._layer_view.getCompatibilityMode())):
start = 0 start = 0
end = 0 end = 0
element_counts = layer_data.getElementCounts() element_counts = layer_data.getElementCounts()
@ -147,7 +147,7 @@ class SimulationPass(RenderPass):
# In the current layer, we show just the indicated paths # In the current layer, we show just the indicated paths
if layer == self._layer_view._current_layer_num: if layer == self._layer_view._current_layer_num:
# We look for the position of the head, searching the point of the current path # We look for the position of the head, searching the point of the current path
index = self._layer_view._current_path_num index = int(self._layer_view.getCurrentPath())
offset = 0 offset = 0
for polygon in layer_data.getLayer(layer).polygons: for polygon in layer_data.getLayer(layer).polygons:
# The size indicates all values in the two-dimension array, and the second dimension is # The size indicates all values in the two-dimension array, and the second dimension is
@ -157,23 +157,33 @@ class SimulationPass(RenderPass):
offset = 1 # This is to avoid the first point when there is more than one polygon, since has the same value as the last point in the previous polygon offset = 1 # This is to avoid the first point when there is more than one polygon, since has the same value as the last point in the previous polygon
continue continue
# The head position is calculated and translated # The head position is calculated and translated
head_position = Vector(polygon.data[index+offset][0], polygon.data[index+offset][1], polygon.data[index+offset][2]) + node.getWorldPosition() ratio = self._layer_view.getCurrentPath() - index
pos_a = Vector(polygon.data[index + offset][0], polygon.data[index + offset][1],
polygon.data[index + offset][2])
if ratio > 0.0001:
pos_b = Vector(polygon.data[index + offset + 1][0],
polygon.data[index + offset + 1][1],
polygon.data[index + offset + 1][2])
vec = pos_a * (1.0 - ratio) + pos_b * ratio
head_position = vec + node.getWorldPosition()
else:
head_position = pos_a + node.getWorldPosition()
break break
break break
if self._layer_view._minimum_layer_num > layer: if self._layer_view.getMinimumLayer() > layer:
start += element_counts[layer] start += element_counts[layer]
end += element_counts[layer] end += element_counts[layer]
# Calculate the range of paths in the last layer # Calculate the range of paths in the last layer
current_layer_start = end current_layer_start = end
current_layer_end = end + self._layer_view._current_path_num * 2 # Because each point is used twice current_layer_end = end + int( self._layer_view.getCurrentPath()) * 2 # Because each point is used twice
# This uses glDrawRangeElements internally to only draw a certain range of lines. # This uses glDrawRangeElements internally to only draw a certain range of lines.
# All the layers but the current selected layer are rendered first # All the layers but the current selected layer are rendered first
if self._old_current_path != self._layer_view._current_path_num: if self._old_current_path != self._layer_view.getCurrentPath():
self._current_shader = self._layer_shadow_shader self._current_shader = self._layer_shadow_shader
self._switching_layers = False self._switching_layers = False
if not self._layer_view.isSimulationRunning() and self._old_current_layer != self._layer_view._current_layer_num: if not self._layer_view.isSimulationRunning() and self._old_current_layer != self._layer_view.getCurrentLayer():
self._current_shader = self._layer_shader self._current_shader = self._layer_shader
self._switching_layers = True self._switching_layers = True
@ -193,8 +203,8 @@ class SimulationPass(RenderPass):
current_layer_batch.addItem(node.getWorldTransformation(), layer_data) current_layer_batch.addItem(node.getWorldTransformation(), layer_data)
current_layer_batch.render(self._scene.getActiveCamera()) current_layer_batch.render(self._scene.getActiveCamera())
self._old_current_layer = self._layer_view._current_layer_num self._old_current_layer = self._layer_view.getCurrentLayer()
self._old_current_path = self._layer_view._current_path_num self._old_current_path = self._layer_view.getCurrentPath()
# Create a new batch that is not range-limited # Create a new batch that is not range-limited
batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid) batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid)
@ -230,4 +240,4 @@ class SimulationPass(RenderPass):
if changed_object.callDecoration("getLayerData"): # Any layer data has changed. if changed_object.callDecoration("getLayerData"): # Any layer data has changed.
self._switching_layers = True self._switching_layers = True
self._old_current_layer = 0 self._old_current_layer = 0
self._old_current_path = 0 self._old_current_path = 0.0

View file

@ -40,7 +40,7 @@ from .SimulationViewProxy import SimulationViewProxy
import numpy import numpy
import os.path import os.path
from typing import Optional, TYPE_CHECKING, List, cast from typing import Optional, TYPE_CHECKING, List, Tuple, cast
if TYPE_CHECKING: if TYPE_CHECKING:
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
@ -74,21 +74,20 @@ class SimulationView(CuraView):
self._old_max_layers = 0 self._old_max_layers = 0
self._max_paths = 0 self._max_paths = 0
self._current_path_num = 0 self._current_path_num: float = 0.0
self._current_time = 0.0
self._minimum_path_num = 0 self._minimum_path_num = 0
self.currentLayerNumChanged.connect(self._onCurrentLayerNumChanged) self.currentLayerNumChanged.connect(self._onCurrentLayerNumChanged)
self._current_feedrates = {}
self._lengths_of_polyline ={}
self._busy = False self._busy = False
self._simulation_running = False self._simulation_running = False
self._ghost_shader = None # type: Optional["ShaderProgram"] self._ghost_shader: Optional["ShaderProgram"] = None
self._layer_pass = None # type: Optional[SimulationPass] self._layer_pass: Optional[SimulationPass] = None
self._composite_pass = None # type: Optional[CompositePass] self._composite_pass: Optional[CompositePass] = None
self._old_layer_bindings = None # type: Optional[List[str]] self._old_layer_bindings: Optional[List[str]] = None
self._simulationview_composite_shader = None # type: Optional["ShaderProgram"] self._simulationview_composite_shader: Optional["ShaderProgram"] = None
self._old_composite_shader = None # type: Optional["ShaderProgram"] self._old_composite_shader: Optional["ShaderProgram"] = None
self._max_feedrate = sys.float_info.min self._max_feedrate = sys.float_info.min
self._min_feedrate = sys.float_info.max self._min_feedrate = sys.float_info.max
@ -99,13 +98,13 @@ class SimulationView(CuraView):
self._min_flow_rate = sys.float_info.max self._min_flow_rate = sys.float_info.max
self._max_flow_rate = sys.float_info.min self._max_flow_rate = sys.float_info.min
self._global_container_stack = None # type: Optional[ContainerStack] self._global_container_stack: Optional[ContainerStack] = None
self._proxy = None self._proxy = None
self._resetSettings() self._resetSettings()
self._legend_items = None self._legend_items = None
self._show_travel_moves = False self._show_travel_moves = False
self._nozzle_node = None # type: Optional[NozzleNode] self._nozzle_node: Optional[NozzleNode] = None
Application.getInstance().getPreferences().addPreference("view/top_layer_count", 5) Application.getInstance().getPreferences().addPreference("view/top_layer_count", 5)
Application.getInstance().getPreferences().addPreference("view/only_show_top_layers", False) Application.getInstance().getPreferences().addPreference("view/only_show_top_layers", False)
@ -127,13 +126,12 @@ class SimulationView(CuraView):
self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers")) self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers"))
self._compatibility_mode = self._evaluateCompatibilityMode() self._compatibility_mode = self._evaluateCompatibilityMode()
self._slice_first_warning_message = Message(catalog.i18nc("@info:status", self._slice_first_warning_message = Message(catalog.i18nc("@info:status", "Nothing is shown because you need to slice first."),
"Nothing is shown because you need to slice first."), title=catalog.i18nc("@info:title", "No layers to show"),
title = catalog.i18nc("@info:title", "No layers to show"), option_text=catalog.i18nc("@info:option_text",
option_text = catalog.i18nc("@info:option_text", "Do not show this message again"),
"Do not show this message again"), option_state=False,
option_state = False, message_type=Message.MessageType.WARNING)
message_type = Message.MessageType.WARNING)
self._slice_first_warning_message.optionToggled.connect(self._onDontAskMeAgain) self._slice_first_warning_message.optionToggled.connect(self._onDontAskMeAgain)
CuraApplication.getInstance().getPreferences().addPreference(self._no_layers_warning_preference, True) CuraApplication.getInstance().getPreferences().addPreference(self._no_layers_warning_preference, True)
@ -189,9 +187,82 @@ class SimulationView(CuraView):
def getMaxLayers(self) -> int: def getMaxLayers(self) -> int:
return self._max_layers return self._max_layers
def getCurrentPath(self) -> int: def getCurrentPath(self) -> float:
return self._current_path_num return self._current_path_num
def setTime(self, time: float) -> None:
self._current_time = time
left_i = 0
right_i = self._max_paths - 1
total_duration, cumulative_line_duration = self.cumulativeLineDuration()
# make an educated guess about where to start
i = int(right_i * max(0.0, min(1.0, self._current_time / total_duration)))
# binary search for the correct path
while left_i < right_i:
if cumulative_line_duration[i] <= self._current_time:
left_i = i + 1
else:
right_i = i
i = int((left_i + right_i) / 2)
left_value = cumulative_line_duration[i - 1] if i > 0 else 0.0
right_value = cumulative_line_duration[i]
assert (left_value <= self._current_time <= right_value)
fractional_value = (self._current_time - left_value) / (right_value - left_value)
self.setPath(i + fractional_value)
def advanceTime(self, time_increase: float) -> bool:
"""
Advance the time by the given amount.
:param time_increase: The amount of time to advance (in seconds).
:return: True if the time was advanced, False if the end of the simulation was reached.
"""
total_duration, cumulative_line_duration = self.cumulativeLineDuration()
# time ratio
time_increase = time_increase
if self._current_time + time_increase > total_duration:
# If we have reached the end of the simulation, go to the next layer.
if self.getCurrentLayer() == self.getMaxLayers():
# If we are already at the last layer, go to the first layer.
self.setTime(total_duration)
return False
# advance to the next layer, and reset the time
self.setLayer(self.getCurrentLayer() + 1)
self.setTime(0.0)
else:
self.setTime(self._current_time + time_increase)
return True
def cumulativeLineDuration(self) -> Tuple[float, List[float]]:
# TODO: cache the total duration and cumulative line duration at each layer change event
cumulative_line_duration = []
total_duration = 0.0
for polyline in self.getLayerData().polygons:
for line_duration in list((polyline.lineLengths / polyline.lineFeedrates)[0]):
total_duration += line_duration
cumulative_line_duration.append(total_duration)
return total_duration, cumulative_line_duration
def getLayerData(self) -> Optional["LayerData"]:
scene = self.getController().getScene()
for node in DepthFirstIterator(scene.getRoot()): # type: ignore
layer_data = node.callDecoration("getLayerData")
if not layer_data:
continue
return layer_data.getLayer(self.getCurrentLayer())
return None
def getMinimumPath(self) -> int: def getMinimumPath(self) -> int:
return self._minimum_path_num return self._minimum_path_num
@ -279,7 +350,7 @@ class SimulationView(CuraView):
self._startUpdateTopLayers() self._startUpdateTopLayers()
self.currentLayerNumChanged.emit() self.currentLayerNumChanged.emit()
def setPath(self, value: int) -> None: def setPath(self, value: float) -> None:
""" """
Set the upper end of the range of visible paths on the current layer. Set the upper end of the range of visible paths on the current layer.
@ -402,15 +473,6 @@ class SimulationView(CuraView):
def getMaxFeedrate(self) -> float: def getMaxFeedrate(self) -> float:
return self._max_feedrate return self._max_feedrate
def getSimulationTime(self, currentIndex) -> float:
try:
return (self._lengths_of_polyline[self._current_layer_num][currentIndex] / self._current_feedrates[self._current_layer_num][currentIndex])[0]
except:
# In case of change in layers, currentIndex comes one more than the items in the lengths_of_polyline
# We give 1 second time for layer change
return 1.0
def getMinThickness(self) -> float: def getMinThickness(self) -> float:
if abs(self._min_thickness - sys.float_info.max) < 10: # Some lenience due to floating point rounding. if abs(self._min_thickness - sys.float_info.max) < 10: # Some lenience due to floating point rounding.
return 0.0 # If it's still max-float, there are no measurements. Use 0 then. return 0.0 # If it's still max-float, there are no measurements. Use 0 then.
@ -535,10 +597,8 @@ class SimulationView(CuraView):
visible_indicies_with_extrusion = numpy.where(numpy.isin(polyline.types, visible_line_types_with_extrusion))[0] visible_indicies_with_extrusion = numpy.where(numpy.isin(polyline.types, visible_line_types_with_extrusion))[0]
if visible_indices.size == 0: # No items to take maximum or minimum of. if visible_indices.size == 0: # No items to take maximum or minimum of.
continue continue
self._lengths_of_polyline[layer_index] = polyline.lineLengths
visible_feedrates = numpy.take(polyline.lineFeedrates, visible_indices) visible_feedrates = numpy.take(polyline.lineFeedrates, visible_indices)
visible_feedrates_with_extrusion = numpy.take(polyline.lineFeedrates, visible_indicies_with_extrusion) visible_feedrates_with_extrusion = numpy.take(polyline.lineFeedrates, visible_indicies_with_extrusion)
self._current_feedrates[layer_index] = polyline.lineFeedrates
visible_linewidths = numpy.take(polyline.lineWidths, visible_indices) visible_linewidths = numpy.take(polyline.lineWidths, visible_indices)
visible_linewidths_with_extrusion = numpy.take(polyline.lineWidths, visible_indicies_with_extrusion) visible_linewidths_with_extrusion = numpy.take(polyline.lineWidths, visible_indicies_with_extrusion)
visible_thicknesses = numpy.take(polyline.lineThicknesses, visible_indices) visible_thicknesses = numpy.take(polyline.lineThicknesses, visible_indices)

View file

@ -136,54 +136,19 @@ Item
Timer Timer
{ {
id: simulationTimer id: simulationTimer
interval: UM.SimulationView.simulationTime interval: 1000 / 60
running: false running: false
repeat: true repeat: true
onTriggered: onTriggered:
{ {
var currentPath = UM.SimulationView.currentPath // divide by 1000 to accont for ms to s conversion
var numPaths = UM.SimulationView.numPaths const advance_time = simulationTimer.interval / 1000.0;
var currentLayer = UM.SimulationView.currentLayer if (!UM.SimulationView.advanceTime(advance_time)) {
var numLayers = UM.SimulationView.numLayers playButton.pauseSimulation();
// When the user plays the simulation, if the path slider is at the end of this layer, we start
// the simulation at the beginning of the current layer.
if (!isSimulationPlaying)
{
if (currentPath >= numPaths)
{
UM.SimulationView.setCurrentPath(0)
}
else
{
UM.SimulationView.setCurrentPath(currentPath + 1)
}
}
// If the simulation is already playing and we reach the end of a layer, then it automatically
// starts at the beginning of the next layer.
else
{
if (currentPath >= numPaths)
{
// At the end of the model, the simulation stops
if (currentLayer >= numLayers)
{
playButton.pauseSimulation()
}
else
{
UM.SimulationView.setCurrentLayer(currentLayer + 1)
UM.SimulationView.setCurrentPath(0)
}
}
else
{
UM.SimulationView.setCurrentPath(currentPath + 1)
}
} }
// The status must be set here instead of in the resumeSimulation function otherwise it won't work // The status must be set here instead of in the resumeSimulation function otherwise it won't work
// correctly, because part of the logic is in this trigger function. // correctly, because part of the logic is in this trigger function.
isSimulationPlaying = true isSimulationPlaying = true;
} }
} }

View file

@ -2,7 +2,6 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import numpy
from PyQt6.QtCore import QObject, pyqtSignal, pyqtProperty from PyQt6.QtCore import QObject, pyqtSignal, pyqtProperty
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
from UM.Application import Application from UM.Application import Application
@ -12,11 +11,6 @@ if TYPE_CHECKING:
class SimulationViewProxy(QObject): class SimulationViewProxy(QObject):
S_TO_MS = 1000
SPEED_OF_SIMULATION = 10
FACTOR = S_TO_MS/SPEED_OF_SIMULATION
def __init__(self, simulation_view: "SimulationView", parent=None) -> None: def __init__(self, simulation_view: "SimulationView", parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self._simulation_view = simulation_view self._simulation_view = simulation_view
@ -56,17 +50,13 @@ class SimulationViewProxy(QObject):
def numPaths(self): def numPaths(self):
return self._simulation_view.getMaxPaths() return self._simulation_view.getMaxPaths()
@pyqtProperty(int, notify=currentPathChanged) @pyqtProperty(float, notify=currentPathChanged)
def currentPath(self): def currentPath(self):
return self._simulation_view.getCurrentPath() return self._simulation_view.getCurrentPath()
@pyqtProperty(int, notify=currentPathChanged) @pyqtSlot(float, result=bool)
def simulationTime(self): def advanceTime(self, duration: float) -> bool:
# Extracts the currents paths simulation time (in seconds) for the current path from the dict of simulation time of the current layer. return self._simulation_view.advanceTime(duration)
# We multiply the time with 100 to make it to ms from s.(Should be 1000 in real time). This scaling makes the simulation time 10x faster than the real time.
simulationTimeOfpath = self._simulation_view.getSimulationTime(self._simulation_view.getCurrentPath()) * SimulationViewProxy.FACTOR
# Since the timer cannot process time less than 1 ms, we put a lower limit here
return int(max(1, simulationTimeOfpath))
@pyqtProperty(int, notify=currentPathChanged) @pyqtProperty(int, notify=currentPathChanged)
def minimumPath(self): def minimumPath(self):
@ -92,8 +82,8 @@ class SimulationViewProxy(QObject):
def setMinimumLayer(self, layer_num): def setMinimumLayer(self, layer_num):
self._simulation_view.setMinimumLayer(layer_num) self._simulation_view.setMinimumLayer(layer_num)
@pyqtSlot(int) @pyqtSlot(float)
def setCurrentPath(self, path_num): def setCurrentPath(self, path_num: float):
self._simulation_view.setPath(path_num) self._simulation_view.setPath(path_num)
@pyqtSlot(int) @pyqtSlot(int)
@ -229,4 +219,3 @@ class SimulationViewProxy(QObject):
self._simulation_view.activityChanged.disconnect(self._onActivityChanged) self._simulation_view.activityChanged.disconnect(self._onActivityChanged)
self._simulation_view.globalStackChanged.disconnect(self._onGlobalStackChanged) self._simulation_view.globalStackChanged.disconnect(self._onGlobalStackChanged)
self._simulation_view.preferencesChanged.disconnect(self._onPreferencesChanged) self._simulation_view.preferencesChanged.disconnect(self._onPreferencesChanged)