Skip to content

Commit e5508f5

Browse files
authored
Merge pull request #21201 from Ultimaker/SimulationView_display_height_plugin_update
Show layer height in Simulation View slider labels
2 parents 664e42c + f5a9cb2 commit e5508f5

File tree

5 files changed

+123
-1
lines changed

5 files changed

+123
-1
lines changed

plugins/SimulationView/LayerSlider.qml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ Item
274274
value: sliderRoot.upperValue
275275
busy: UM.SimulationView.busy
276276
setValue: upperHandle.setValueManually // connect callback functions
277+
layerHeight: UM.SimulationView.currentLayerHeight
277278
}
278279
}
279280

@@ -384,6 +385,7 @@ Item
384385
value: sliderRoot.lowerValue
385386
busy: UM.SimulationView.busy
386387
setValue: lowerHandle.setValueManually // connect callback functions
388+
layerHeight: UM.SimulationView.minimumLayerHeight
387389
}
388390
}
389391
}

plugins/SimulationView/SimulationSliderLabel.qml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ UM.PointingRectangle
1818
property var setValue // Function
1919
property bool busy: false
2020
property int startFrom: 1
21+
property real layerHeight: 0.0 // Height in mm to display
2122

2223
target: Qt.point(parent.width, y + height / 2)
2324
arrowSize: UM.Theme.getSize("button_tooltip_arrow").height
@@ -84,6 +85,29 @@ UM.PointingRectangle
8485
bottom: startFrom
8586
top: sliderLabelRoot.maximumValue + startFrom // +startFrom because maybe we want to start in a different value rather than 0
8687
}
88+
89+
Rectangle
90+
{
91+
id: layerHeightBackground
92+
x: -(width + UM.Theme.getSize("narrow_margin").width)
93+
y: (parent.height - height) / 2
94+
width: layerHeightText.width + 2 * UM.Theme.getSize("narrow_margin").width
95+
height: layerHeightText.height + 2 * UM.Theme.getSize("narrow_margin").height
96+
color: UM.Theme.getColor("tool_panel_background")
97+
radius: UM.Theme.getSize("default_radius").width
98+
border.color: UM.Theme.getColor("lining")
99+
border.width: UM.Theme.getSize("default_lining").width
100+
101+
Text
102+
{
103+
id: layerHeightText
104+
anchors.centerIn: parent
105+
text: sliderLabelRoot.layerHeight.toFixed(2) + "mm"
106+
color: UM.Theme.getColor("text")
107+
font: UM.Theme.getFont("default")
108+
renderType: Text.NativeRendering
109+
}
110+
}
87111
}
88112
BusyIndicator
89113
{

plugins/SimulationView/SimulationView.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ def __init__(self, parent = None) -> None:
101101
self._cumulative_line_duration_layer: Optional[int] = None
102102
self._cumulative_line_duration: List[float] = []
103103

104+
# Cache for layer heights to avoid recalculating on every query
105+
self._layer_heights_cache: dict[int, float] = {}
106+
self._layer_heights_cache_node_id: Optional[int] = None # Track which node's data is cached
107+
104108
self._global_container_stack: Optional[ContainerStack] = None
105109
self._proxy = None
106110

@@ -289,6 +293,88 @@ def getLayerData(self) -> Optional["LayerData"]:
289293
return layer_data.getLayer(self.getCurrentLayer())
290294
return None
291295

296+
def _calculateLayerHeightsCache(self) -> None:
297+
"""Calculate and cache heights for all layers.
298+
299+
This method iterates through all layers once and stores their heights in a cache.
300+
Handles both sliced data (microns) and loaded gcode (millimeters).
301+
For layer 0 from gcode, uses thickness instead of height due to incorrect Z coordinates.
302+
Only recalculates if the layer data source has changed.
303+
"""
304+
scene = self.getController().getScene()
305+
from cura.Scene.GCodeListDecorator import GCodeListDecorator
306+
307+
for node in DepthFirstIterator(scene.getRoot()): # type: ignore
308+
layer_data = node.callDecoration("getLayerData")
309+
if not layer_data:
310+
continue
311+
312+
# Check if we already have cached data for this layer_data object
313+
# Use id of the layer_data itself, not the node, since node might be reused
314+
current_layer_data_id = id(layer_data)
315+
if self._layer_heights_cache_node_id == current_layer_data_id and self._layer_heights_cache:
316+
# Cache is still valid, no need to recalculate
317+
return
318+
# Cache is invalid or empty, recalculate
319+
self._layer_heights_cache.clear()
320+
self._layer_heights_cache_node_id = current_layer_data_id
321+
322+
has_gcode_decorator = node.getDecorator(GCodeListDecorator) is not None
323+
324+
# Process all layers at once
325+
for layer_id in layer_data.getLayers():
326+
layer = layer_data.getLayer(layer_id)
327+
if not layer:
328+
continue
329+
330+
# If node has GCodeListDecorator, heights are already in millimeters (from gcode)
331+
if has_gcode_decorator:
332+
# Special case for layer 0: FlavorParser may get wrong Z coordinate (startup position)
333+
# Use thickness instead, which represents the actual layer height
334+
if layer_id == 0 and layer.thickness > 0:
335+
self._layer_heights_cache[layer_id] = layer.thickness
336+
else:
337+
self._layer_heights_cache[layer_id] = layer.height
338+
# Otherwise, heights are in microns (backend/slicing), convert to mm
339+
else:
340+
self._layer_heights_cache[layer_id] = layer.height / 1000.0
341+
342+
# We found layer data and cached it, no need to continue searching
343+
return
344+
345+
# No layer data found - clear the cache
346+
if self._layer_heights_cache_node_id is not None:
347+
self._layer_heights_cache.clear()
348+
self._layer_heights_cache_node_id = None
349+
350+
def _getLayerHeight(self, layer_number: int) -> float:
351+
"""Helper method to get the height of a specific layer in millimeters from cache.
352+
353+
:param layer_number: The layer number to get the height for.
354+
:return: The layer height in millimeters, or 0.0 if no data is available.
355+
"""
356+
return self._layer_heights_cache.get(layer_number, 0.0)
357+
358+
def getCurrentLayerHeight(self) -> float:
359+
"""Get the height (z-coordinate) of the current layer in millimeters.
360+
361+
This returns the actual height from the layer data, which already takes into account:
362+
- Initial layer height (layer_height_0)
363+
- Adaptive layer heights
364+
- Regular layer height
365+
- Raft layers
366+
367+
Returns 0.0 if no layer data is available.
368+
"""
369+
return self._layer_heights_cache.get(self.getCurrentLayer(), 0.0)
370+
371+
def getMinimumLayerHeight(self) -> float:
372+
"""Get the height (z-coordinate) of the minimum layer in millimeters.
373+
374+
Returns 0.0 if no layer data is available.
375+
"""
376+
return self._layer_heights_cache.get(self.getMinimumLayer(), 0.0)
377+
292378
def getMinimumPath(self) -> int:
293379
return self._minimum_path_num
294380

@@ -304,6 +390,7 @@ def _onSceneChanged(self, node: "SceneNode") -> None:
304390
if node.getMeshData() is None:
305391
return
306392
self.setActivity(False)
393+
self._calculateLayerHeightsCache()
307394
self.calculateColorSchemeLimits()
308395
self.calculateMaxLayers()
309396
self.calculateMaxPathsOnLayer(self._current_layer_num)
@@ -718,6 +805,7 @@ def event(self, event) -> bool:
718805
Application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
719806
self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged)
720807

808+
self._calculateLayerHeightsCache()
721809
self.calculateColorSchemeLimits()
722810
self.calculateMaxLayers()
723811
self.calculateMaxPathsOnLayer(self._current_layer_num)

plugins/SimulationView/SimulationViewProxy.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ def currentLayer(self):
4646
def minimumLayer(self):
4747
return self._simulation_view.getMinimumLayer()
4848

49+
@pyqtProperty(float, notify=currentLayerChanged)
50+
def currentLayerHeight(self):
51+
return self._simulation_view.getCurrentLayerHeight()
52+
53+
@pyqtProperty(float, notify=currentLayerChanged)
54+
def minimumLayerHeight(self):
55+
return self._simulation_view.getMinimumLayerHeight()
56+
4957
@pyqtProperty(int, notify=maxPathsChanged)
5058
def numPaths(self):
5159
return self._simulation_view.getMaxPaths()

plugins/SimulationView/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "Simulation View",
33
"author": "Ultimaker B.V.",
4-
"version": "1.0.2",
4+
"version": "1.1.0",
55
"description": "Provides the preview of sliced layerdata.",
66
"api": 8,
77
"i18n-catalog": "cura"

0 commit comments

Comments
 (0)