fix: adaptive layer height profile uses uncompensated Z height #965

Closed
opened 2026-04-05 16:38:26 +02:00 by MrUnknownDE · 0 comments
Owner

Originally created by @mrmees on 1/26/2026

Closes #10388

Summary

  • Adaptive layer heights are silently discarded when shrinkage compensation is active
  • Root cause: mismatch between the height reference used by the profile generator vs. the profile validator
  • Fix: 3 line changes in src/libslic3r/Slicing.cpp

Bug Description

When a filament or printer profile has XYZ shrinkage/scaling compensation enabled (even small values like 0.4%), clicking "Adaptive Layer Height" in the layer editing panel produces flat (uniform) layers instead of the expected variable-height adaptive profile.

The bug is deterministic whenever shrinkage_compensation_z != 1.0, but was reported as intermittent because it depends on which filament/printer profile is active. On systems/profiles without shrinkage compensation, adaptive layers work normally.

Observed behavior

  • Fresh load, no shrinkage compensation: Adaptive layers work correctly.
  • Fresh load, shrinkage compensation active: Adaptive profile is generated then immediately replaced with flat layers on the next render frame.
  • After rotation/movement: The model's bounding box height changes, causing select_object() to invalidate the cached profile and slicing parameters. The old adaptive profile's Z extent no longer matches the new object height, so it fails validation and is cleared. Clicking "Adaptive Layer Height" again to regenerate fails because of the shrinkage mismatch — every new profile is immediately invalidated.

The rotation/movement behavior made this appear like a geometry-triggered bug, but rotation merely exposed the underlying issue by forcing a regeneration cycle that could never produce a valid profile.

Root Cause

How the adaptive profile is generated

layer_height_profile_adaptive() in Slicing.cpp builds a flat vector of [z, height, z, height, ...] pairs representing variable layer heights. The Z coordinates in this profile represent positions on the raw model geometry (uncompensated space).

How the profile is validated

PrintObject::update_layer_height_profile() in PrintObject.cpp (line 3475) validates that the profile's final Z coordinate matches object_print_z_uncompensated_max within a 1e-3 tolerance. If validation fails, the profile is silently cleared and replaced with flat layers from layer_height_profile_from_ranges().

The mismatch

layer_height_profile_adaptive() was using slicing_params.object_print_z_height() to bound its output — this returns the shrinkage-compensated height (object_height * shrinkage_compensation_z). But the validator expects the uncompensated height.

For example, with a 14.15 mm tall object and shrinkage_compensation_z = 1.004:

  • Profile generated with max Z = 14.15 * 1.004 = 14.2066 mm
  • Validator expected max Z = 14.15 mm
  • Delta = 0.0566 mm > 0.001 mm tolerance → profile cleared

This happened every render frame via generate_layer_height_texture()update_layer_height_profile(), so the adaptive profile was generated and immediately discarded in a continuous loop.

Why layer_height_profile_from_ranges() was unaffected

The other profile generator, layer_height_profile_from_ranges(), already correctly uses object_print_z_uncompensated_height() (lines 233/236). Only the adaptive path had this bug.

How shrinkage compensation is actually applied

Downstream, generate_object_layers() reads profile Z values and explicitly multiplies them by shrinkage_compensation_z to produce actual print layer boundaries (lines 777-782). This means the profile must be in uncompensated space — using compensated values results in double-scaling.

Debugging Process

  1. Added diagnostic logging to four files (GLCanvas3D.cpp, PrintObject.cpp, Slicing.cpp, SlicingAdaptive.cpp) with [AdaptiveLayerDbg] tags to trace profile generation, validation, and invalidation in real time.

  2. Built and tested with a model that exhibited the bug.

  3. Log output revealed the validation failure repeating every frame:

    [AdaptiveLayerDbg] VALIDATION FAILED: last_z=14.2068 expected_z=14.15 delta=0.0568273
    
  4. Traced the delta to the difference between object_print_z_height() (14.2068, compensated) and object_print_z_uncompensated_max (14.15, raw) in SlicingParameters.

  5. Confirmed the fix by changing 3 references from object_print_z_height() to object_print_z_uncompensated_height(). All diagnostic logging was then removed to produce a minimal diff.

The Fix

Three substitutions in layer_height_profile_adaptive() in src/libslic3r/Slicing.cpp:

Location Context Before After
Line 263 Loop termination object_print_z_height() object_print_z_uncompensated_height()
Line 334 Gap calculation object_print_z_height() object_print_z_uncompensated_height()
Line 337 Final profile entry object_print_z_height() object_print_z_uncompensated_height()

Downstream Impact Analysis

Zero impact when shrinkage compensation is 1.0 (default)

Both methods return the same value. No behavior change for users without shrinkage compensation.

Impact when shrinkage compensation is active

Component Impact Explanation
Profile generation Fixed Profile now bounded by correct uncompensated extent
Profile validation (update_layer_height_profile) Fixed Profile passes the existing 1e-3 tolerance check
Profile storage (ModelObject::layer_height_profile) No change Stores profiles in uncompensated space as before
Layer generation (generate_object_layers) Correct Applies shrinkage_compensation_z scaling once, no double-scaling
Actual slicing (PrintObjectSlice) Correct Receives properly compensated layer boundaries
Build volume validation Correct Checks compensated height against printable height
Layer height texture rendering Unaffected Uses object_print_z_height() independently for texture sizing
Consistency with layer_height_profile_from_ranges() Restored Both profile generators now use the same height reference

Regression risk: None identified

All downstream consumers expect profile Z values in uncompensated space. The fix aligns the adaptive generator with this existing convention. No downstream code changes required.

Test Plan

  • Load a model with shrinkage compensation enabled → click Adaptive Layer Height → verify variable layers appear
  • Load a model without shrinkage compensation → click Adaptive Layer Height → verify behavior unchanged
  • Rotate/move model with shrinkage compensation → click Adaptive again → verify adaptive layers persist
  • Slice with adaptive layers + shrinkage compensation → verify G-code layer heights are correct
  • Verify manual layer editing (layer_height_profile_from_ranges()) still works correctly
*Originally created by @mrmees on 1/26/2026* Closes #10388 ## Summary - Adaptive layer heights are silently discarded when shrinkage compensation is active - Root cause: mismatch between the height reference used by the profile generator vs. the profile validator - Fix: 3 line changes in `src/libslic3r/Slicing.cpp` ## Bug Description When a filament or printer profile has XYZ shrinkage/scaling compensation enabled (even small values like 0.4%), clicking "Adaptive Layer Height" in the layer editing panel produces flat (uniform) layers instead of the expected variable-height adaptive profile. The bug is deterministic whenever `shrinkage_compensation_z != 1.0`, but was reported as intermittent because it depends on which filament/printer profile is active. On systems/profiles without shrinkage compensation, adaptive layers work normally. ### Observed behavior - **Fresh load, no shrinkage compensation:** Adaptive layers work correctly. - **Fresh load, shrinkage compensation active:** Adaptive profile is generated then immediately replaced with flat layers on the next render frame. - **After rotation/movement:** The model's bounding box height changes, causing `select_object()` to invalidate the cached profile and slicing parameters. The old adaptive profile's Z extent no longer matches the new object height, so it fails validation and is cleared. Clicking "Adaptive Layer Height" again to regenerate fails because of the shrinkage mismatch — every new profile is immediately invalidated. The rotation/movement behavior made this appear like a geometry-triggered bug, but rotation merely exposed the underlying issue by forcing a regeneration cycle that could never produce a valid profile. ## Root Cause ### How the adaptive profile is generated `layer_height_profile_adaptive()` in `Slicing.cpp` builds a flat vector of `[z, height, z, height, ...]` pairs representing variable layer heights. The Z coordinates in this profile represent positions on the raw model geometry (uncompensated space). ### How the profile is validated `PrintObject::update_layer_height_profile()` in `PrintObject.cpp` (line 3475) validates that the profile's final Z coordinate matches `object_print_z_uncompensated_max` within a 1e-3 tolerance. If validation fails, the profile is silently cleared and replaced with flat layers from `layer_height_profile_from_ranges()`. ### The mismatch `layer_height_profile_adaptive()` was using `slicing_params.object_print_z_height()` to bound its output — this returns the shrinkage-compensated height (`object_height * shrinkage_compensation_z`). But the validator expects the uncompensated height. For example, with a 14.15 mm tall object and `shrinkage_compensation_z = 1.004`: - Profile generated with max Z = `14.15 * 1.004 = 14.2066 mm` - Validator expected max Z = `14.15 mm` - Delta = `0.0566 mm` > `0.001 mm` tolerance → profile cleared This happened every render frame via `generate_layer_height_texture()` → `update_layer_height_profile()`, so the adaptive profile was generated and immediately discarded in a continuous loop. ### Why `layer_height_profile_from_ranges()` was unaffected The other profile generator, `layer_height_profile_from_ranges()`, already correctly uses `object_print_z_uncompensated_height()` (lines 233/236). Only the adaptive path had this bug. ### How shrinkage compensation is actually applied Downstream, `generate_object_layers()` reads profile Z values and explicitly multiplies them by `shrinkage_compensation_z` to produce actual print layer boundaries (lines 777-782). This means the profile must be in uncompensated space — using compensated values results in double-scaling. ## Debugging Process 1. Added diagnostic logging to four files (`GLCanvas3D.cpp`, `PrintObject.cpp`, `Slicing.cpp`, `SlicingAdaptive.cpp`) with `[AdaptiveLayerDbg]` tags to trace profile generation, validation, and invalidation in real time. 2. Built and tested with a model that exhibited the bug. 3. Log output revealed the validation failure repeating every frame: ``` [AdaptiveLayerDbg] VALIDATION FAILED: last_z=14.2068 expected_z=14.15 delta=0.0568273 ``` 4. Traced the delta to the difference between `object_print_z_height()` (14.2068, compensated) and `object_print_z_uncompensated_max` (14.15, raw) in `SlicingParameters`. 5. Confirmed the fix by changing 3 references from `object_print_z_height()` to `object_print_z_uncompensated_height()`. All diagnostic logging was then removed to produce a minimal diff. ## The Fix Three substitutions in `layer_height_profile_adaptive()` in `src/libslic3r/Slicing.cpp`: | Location | Context | Before | After | |----------|---------|--------|-------| | Line 263 | Loop termination | `object_print_z_height()` | `object_print_z_uncompensated_height()` | | Line 334 | Gap calculation | `object_print_z_height()` | `object_print_z_uncompensated_height()` | | Line 337 | Final profile entry | `object_print_z_height()` | `object_print_z_uncompensated_height()` | ## Downstream Impact Analysis ### Zero impact when shrinkage compensation is 1.0 (default) Both methods return the same value. No behavior change for users without shrinkage compensation. ### Impact when shrinkage compensation is active | Component | Impact | Explanation | |-----------|--------|-------------| | Profile generation | Fixed | Profile now bounded by correct uncompensated extent | | Profile validation (`update_layer_height_profile`) | Fixed | Profile passes the existing 1e-3 tolerance check | | Profile storage (`ModelObject::layer_height_profile`) | No change | Stores profiles in uncompensated space as before | | Layer generation (`generate_object_layers`) | Correct | Applies `shrinkage_compensation_z` scaling once, no double-scaling | | Actual slicing (`PrintObjectSlice`) | Correct | Receives properly compensated layer boundaries | | Build volume validation | Correct | Checks compensated height against printable height | | Layer height texture rendering | Unaffected | Uses `object_print_z_height()` independently for texture sizing | | Consistency with `layer_height_profile_from_ranges()` | Restored | Both profile generators now use the same height reference | ### Regression risk: None identified All downstream consumers expect profile Z values in uncompensated space. The fix aligns the adaptive generator with this existing convention. No downstream code changes required. ## Test Plan - [ ] Load a model with shrinkage compensation enabled → click Adaptive Layer Height → verify variable layers appear - [ ] Load a model without shrinkage compensation → click Adaptive Layer Height → verify behavior unchanged - [ ] Rotate/move model with shrinkage compensation → click Adaptive again → verify adaptive layers persist - [ ] Slice with adaptive layers + shrinkage compensation → verify G-code layer heights are correct - [ ] Verify manual layer editing (`layer_height_profile_from_ranges()`) still works correctly
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github/OrcaSlicer#965