git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit53218c4ffd8743d28479874e3d9dcabe7ea02f19
parent41a5c06739
authorLucas Galante <[email protected]>
date2026-08-17 08:34
fix: predict from the knob curve only when a LUT is actually installed

ProfileKnobs seeded `custom` from "config carried saved knobs" — but Save
writes the knob triples as a ride-along even for an untouched section
(so the editor reopens where it was left) while writing the identity
SPEC, which installs nothing. So after any Save, every later launch
predicted from the knob curve while the renderer ran its analytic
branch. The wall curve hid the split (the knob midpoints ARE the
analytic smoothstep); the roll exposed it (the knob family is nothing
like the superellipse quadrant): the predicted band dipped to 82 where
the render sat flat at 101-104. Worse, Save from that state would have
written a real smoothstep-roll spec — installing a roll nobody shaped
DE-wide.

`custom` now seeds from the installed-LUT state
(layout::*_profile_slopes().is_some()), the shader's own condition.

Shadow-measured: analytic roll band agreement 19-24 grey levels wrong ->
max 1 (mean 0.02); after a knob drag both bands track the LUT together
(mean 0.24, the lone 10 at the silhouette column is its 1px AA fade);
wall shapes still <=1 (mean 0.09).

Co-Authored-By: Claude <[email protected]>

 src/bin/cce-relief.rs | 24 ++++++++++++++++++------
 1 file changed, 18 insertions(+), 6 deletions(-)

diff --git a/src/bin/cce-relief.rs b/src/bin/cce-relief.rs
index 3ea53d3..492a8c0 100644
--- a/src/bin/cce-relief.rs
+++ b/src/bin/cce-relief.rs
@@ -350,15 +350,27 @@ struct ProfileKnobs {
     shoulder: Adapted<Slider>,
     base: Adapted<Slider>,
     bias: Adapted<Slider>,
-    /// False until a knob moves (or config carried saved knobs): the DE
-    /// renders its analytic profile and Save writes the identity sentinel.
+    /// False until a knob moves or config installed a real (non-identity)
+    /// profile for this section: the DE renders its analytic profile and
+    /// Save writes the identity sentinel.
     custom: bool,
     /// Last spec applied+logged.
     last_spec: String,
 }
 
 impl ProfileKnobs {
-    fn new(seed: Option<(f32, f32, f32)>) -> Self {
+    /// `installed` is whether the live material actually carries a custom
+    /// LUT for this profile (`layout::*_profile_slopes().is_some()` — the
+    /// shader's own condition). It is NOT the same as "config carried saved
+    /// knobs": Save writes the knob triples as a ride-along even for an
+    /// untouched section (so the editor reopens where it was left), while
+    /// writing the identity SPEC — which installs nothing. Seeding `custom`
+    /// from the knobs' presence made the prediction follow the knob curve
+    /// while the renderer ran analytic. The wall curve hid it (the knob
+    /// midpoints ARE the analytic smoothstep); the roll exposed it (the knob
+    /// family is nothing like the superellipse quadrant) — and a Save from
+    /// that state would have installed a smoothstep roll DE-wide unasked.
+    fn new(seed: Option<(f32, f32, f32)>, installed: bool) -> Self {
         let (s, b, c) = seed.unwrap_or((0.5, 0.5, 0.5));
         let knob = |v: f32, label: &str| {
             Slider::new()
@@ -371,7 +383,7 @@ impl ProfileKnobs {
             shoulder: knob(s, "Shoulder"),
             base: knob(b, "Base"),
             bias: knob(c, "Bias"),
-            custom: seed.is_some(),
+            custom: installed,
             last_spec: String::new(),
         };
         this.last_spec = if this.custom { this.spec() } else { IDENTITY_SPEC.to_string() };
@@ -1155,8 +1167,8 @@ impl Application for BevelPopup {
                 0,
             )
             .with_label("Edge"),
-            wall: ProfileKnobs::new(wall_seed),
-            edge: ProfileKnobs::new(edge_seed),
+            wall: ProfileKnobs::new(wall_seed, cce_ui::layout::bevel_profile_slopes().is_some()),
+            edge: ProfileKnobs::new(edge_seed, cce_ui::layout::roll_profile_slopes().is_some()),
             depth_slider: Slider::new()
                 .with_label("Depth")
                 .with_range(dmin, dmax)