mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-04-18 11:49:35 +08:00
Proof of concept for simulation
Co-authored-by: Casper Lamboo <c.lamboo@ultimaker.com> CURA-7647
This commit is contained in:
parent
3c550557b9
commit
cfec5e0cc1
@ -67,7 +67,7 @@ class LayerPolygon:
|
||||
# Buffering the colors shouldn't be necessary as it is not
|
||||
# re-used and can save a lot of memory usage.
|
||||
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
|
||||
# or type == LayerPolygon.SkinType
|
||||
@ -75,8 +75,8 @@ class LayerPolygon:
|
||||
# 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._build_cache_line_mesh_mask = None # type: Optional[numpy.ndarray]
|
||||
self._build_cache_needed_points = None # type: Optional[numpy.ndarray]
|
||||
self._build_cache_line_mesh_mask: Optional[numpy.ndarray] = None
|
||||
self._build_cache_needed_points: Optional[numpy.ndarray] = None
|
||||
|
||||
def buildCache(self) -> None:
|
||||
# For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out.
|
||||
|
@ -35,7 +35,7 @@ class SimulationPass(RenderPass):
|
||||
self._nozzle_shader = None
|
||||
self._disabled_shader = None
|
||||
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._gl = OpenGL.getInstance().getBindingsObject()
|
||||
self._scene = Application.getInstance().getController().getScene()
|
||||
@ -139,7 +139,7 @@ class SimulationPass(RenderPass):
|
||||
continue
|
||||
|
||||
# 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
|
||||
end = 0
|
||||
element_counts = layer_data.getElementCounts()
|
||||
@ -147,7 +147,7 @@ class SimulationPass(RenderPass):
|
||||
# In the current layer, we show just the indicated paths
|
||||
if layer == self._layer_view._current_layer_num:
|
||||
# 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
|
||||
for polygon in layer_data.getLayer(layer).polygons:
|
||||
# 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
|
||||
continue
|
||||
# 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
|
||||
if self._layer_view._minimum_layer_num > layer:
|
||||
if self._layer_view.getMinimumLayer() > layer:
|
||||
start += element_counts[layer]
|
||||
end += element_counts[layer]
|
||||
|
||||
# Calculate the range of paths in the last layer
|
||||
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.
|
||||
# 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._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._switching_layers = True
|
||||
|
||||
@ -193,8 +203,8 @@ class SimulationPass(RenderPass):
|
||||
current_layer_batch.addItem(node.getWorldTransformation(), layer_data)
|
||||
current_layer_batch.render(self._scene.getActiveCamera())
|
||||
|
||||
self._old_current_layer = self._layer_view._current_layer_num
|
||||
self._old_current_path = self._layer_view._current_path_num
|
||||
self._old_current_layer = self._layer_view.getCurrentLayer()
|
||||
self._old_current_path = self._layer_view.getCurrentPath()
|
||||
|
||||
# Create a new batch that is not range-limited
|
||||
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.
|
||||
self._switching_layers = True
|
||||
self._old_current_layer = 0
|
||||
self._old_current_path = 0
|
||||
self._old_current_path = 0.0
|
||||
|
@ -40,7 +40,7 @@ from .SimulationViewProxy import SimulationViewProxy
|
||||
import numpy
|
||||
import os.path
|
||||
|
||||
from typing import Optional, TYPE_CHECKING, List, cast
|
||||
from typing import Optional, TYPE_CHECKING, List, Tuple, cast
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
@ -74,21 +74,20 @@ class SimulationView(CuraView):
|
||||
self._old_max_layers = 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.currentLayerNumChanged.connect(self._onCurrentLayerNumChanged)
|
||||
|
||||
self._current_feedrates = {}
|
||||
self._lengths_of_polyline ={}
|
||||
self._busy = False
|
||||
self._simulation_running = False
|
||||
|
||||
self._ghost_shader = None # type: Optional["ShaderProgram"]
|
||||
self._layer_pass = None # type: Optional[SimulationPass]
|
||||
self._composite_pass = None # type: Optional[CompositePass]
|
||||
self._old_layer_bindings = None # type: Optional[List[str]]
|
||||
self._simulationview_composite_shader = None # type: Optional["ShaderProgram"]
|
||||
self._old_composite_shader = None # type: Optional["ShaderProgram"]
|
||||
self._ghost_shader: Optional["ShaderProgram"] = None
|
||||
self._layer_pass: Optional[SimulationPass] = None
|
||||
self._composite_pass: Optional[CompositePass] = None
|
||||
self._old_layer_bindings: Optional[List[str]] = None
|
||||
self._simulationview_composite_shader: Optional["ShaderProgram"] = None
|
||||
self._old_composite_shader: Optional["ShaderProgram"] = None
|
||||
|
||||
self._max_feedrate = sys.float_info.min
|
||||
self._min_feedrate = sys.float_info.max
|
||||
@ -99,13 +98,13 @@ class SimulationView(CuraView):
|
||||
self._min_flow_rate = sys.float_info.max
|
||||
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._resetSettings()
|
||||
self._legend_items = None
|
||||
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/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._compatibility_mode = self._evaluateCompatibilityMode()
|
||||
|
||||
self._slice_first_warning_message = Message(catalog.i18nc("@info:status",
|
||||
"Nothing is shown because you need to slice first."),
|
||||
title = catalog.i18nc("@info:title", "No layers to show"),
|
||||
option_text = catalog.i18nc("@info:option_text",
|
||||
"Do not show this message again"),
|
||||
option_state = False,
|
||||
message_type = Message.MessageType.WARNING)
|
||||
self._slice_first_warning_message = Message(catalog.i18nc("@info:status", "Nothing is shown because you need to slice first."),
|
||||
title=catalog.i18nc("@info:title", "No layers to show"),
|
||||
option_text=catalog.i18nc("@info:option_text",
|
||||
"Do not show this message again"),
|
||||
option_state=False,
|
||||
message_type=Message.MessageType.WARNING)
|
||||
self._slice_first_warning_message.optionToggled.connect(self._onDontAskMeAgain)
|
||||
CuraApplication.getInstance().getPreferences().addPreference(self._no_layers_warning_preference, True)
|
||||
|
||||
@ -189,9 +187,82 @@ class SimulationView(CuraView):
|
||||
def getMaxLayers(self) -> int:
|
||||
return self._max_layers
|
||||
|
||||
def getCurrentPath(self) -> int:
|
||||
def getCurrentPath(self) -> float:
|
||||
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:
|
||||
return self._minimum_path_num
|
||||
|
||||
@ -279,7 +350,7 @@ class SimulationView(CuraView):
|
||||
self._startUpdateTopLayers()
|
||||
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.
|
||||
|
||||
@ -402,15 +473,6 @@ class SimulationView(CuraView):
|
||||
def getMaxFeedrate(self) -> float:
|
||||
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:
|
||||
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.
|
||||
@ -535,10 +597,8 @@ class SimulationView(CuraView):
|
||||
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.
|
||||
continue
|
||||
self._lengths_of_polyline[layer_index] = polyline.lineLengths
|
||||
visible_feedrates = numpy.take(polyline.lineFeedrates, visible_indices)
|
||||
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_with_extrusion = numpy.take(polyline.lineWidths, visible_indicies_with_extrusion)
|
||||
visible_thicknesses = numpy.take(polyline.lineThicknesses, visible_indices)
|
||||
|
@ -136,54 +136,19 @@ Item
|
||||
Timer
|
||||
{
|
||||
id: simulationTimer
|
||||
interval: UM.SimulationView.simulationTime
|
||||
interval: 1000 / 60
|
||||
running: false
|
||||
repeat: true
|
||||
onTriggered:
|
||||
{
|
||||
var currentPath = UM.SimulationView.currentPath
|
||||
var numPaths = UM.SimulationView.numPaths
|
||||
var currentLayer = UM.SimulationView.currentLayer
|
||||
var numLayers = UM.SimulationView.numLayers
|
||||
|
||||
// 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)
|
||||
}
|
||||
// divide by 1000 to accont for ms to s conversion
|
||||
const advance_time = simulationTimer.interval / 1000.0;
|
||||
if (!UM.SimulationView.advanceTime(advance_time)) {
|
||||
playButton.pauseSimulation();
|
||||
}
|
||||
// The status must be set here instead of in the resumeSimulation function otherwise it won't work
|
||||
// correctly, because part of the logic is in this trigger function.
|
||||
isSimulationPlaying = true
|
||||
isSimulationPlaying = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy
|
||||
from PyQt6.QtCore import QObject, pyqtSignal, pyqtProperty
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from UM.Application import Application
|
||||
@ -12,11 +11,6 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
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:
|
||||
super().__init__(parent)
|
||||
self._simulation_view = simulation_view
|
||||
@ -56,17 +50,13 @@ class SimulationViewProxy(QObject):
|
||||
def numPaths(self):
|
||||
return self._simulation_view.getMaxPaths()
|
||||
|
||||
@pyqtProperty(int, notify=currentPathChanged)
|
||||
@pyqtProperty(float, notify=currentPathChanged)
|
||||
def currentPath(self):
|
||||
return self._simulation_view.getCurrentPath()
|
||||
|
||||
@pyqtProperty(int, notify=currentPathChanged)
|
||||
def simulationTime(self):
|
||||
# Extracts the currents paths simulation time (in seconds) for the current path from the dict of simulation time of the current layer.
|
||||
# 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))
|
||||
@pyqtSlot(float, result=bool)
|
||||
def advanceTime(self, duration: float) -> bool:
|
||||
return self._simulation_view.advanceTime(duration)
|
||||
|
||||
@pyqtProperty(int, notify=currentPathChanged)
|
||||
def minimumPath(self):
|
||||
@ -92,8 +82,8 @@ class SimulationViewProxy(QObject):
|
||||
def setMinimumLayer(self, layer_num):
|
||||
self._simulation_view.setMinimumLayer(layer_num)
|
||||
|
||||
@pyqtSlot(int)
|
||||
def setCurrentPath(self, path_num):
|
||||
@pyqtSlot(float)
|
||||
def setCurrentPath(self, path_num: float):
|
||||
self._simulation_view.setPath(path_num)
|
||||
|
||||
@pyqtSlot(int)
|
||||
@ -229,4 +219,3 @@ class SimulationViewProxy(QObject):
|
||||
self._simulation_view.activityChanged.disconnect(self._onActivityChanged)
|
||||
self._simulation_view.globalStackChanged.disconnect(self._onGlobalStackChanged)
|
||||
self._simulation_view.preferencesChanged.disconnect(self._onPreferencesChanged)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user