GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
refactor: cce-relief's knobs leave the style block for its own state.kdl
The Shoulder / Base / Bias triples behind each profile spec were
`(bevel)`-typed keys in style.surface.relief (`wall.knobs` /
`edge.knobs`, before that `profile_knobs` / `edge_knobs`), written so
the editor could reopen where it was left: editor state beside the
values that draw, and the one relief key nothing but the editor read.
They live in ~/.config/cce/cce-relief/state.kdl now, one `knobs` node
per Save target — `shared`, a retargeted file's path, or `<path>#<key>`
for a `(relief)` value — because a per-app override and the shared
material are two materials whose sliders must not seed each other
(`knob_state`, with tests). Seeds read the state file first, then a
`(relief)` value's own `k=` ride-along, then a knob key a config still
carries; Save writes the state file and takes the knob keys off the
config under either spelling. The shared config is parsed for seeds too
(only a retargeted one was), since a legacy knob key has no registry
slot left to fall back to: the `bevel_profile_knobs` /
`roll_profile_knobs` registry keys and their flatten arms are gone,
along with the knobs entries of `prefer_relief_wall_edge`.
The `(bevel)` type and `parse_bevel_knobs` stay: a `(relief)` value
still carries `k=`, which is what the data editor's preview draws.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CLAUDE.md | 36 ++++--
src/bin/cce-relief.rs | 233 ++++++++++++++++++++++++++++++--------
src/config.rs | 4 +-
src/layout.rs | 43 ++++---
src/widget/input/bevel_preview.rs | 5 +-
5 files changed, 236 insertions(+), 85 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 659f07b..46214c2 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -680,8 +680,8 @@ multiply on the plate's own fill plus a specular crest. Each is a node under
```kdl
relief depth=0.15 width=9.3 {
- wall height=(mm)0.3 profile="smooth;…" knobs=(bevel)"0.5,0.5,0.5"
- edge height=4.0 profile="smooth;…" knobs=(bevel)"0.5,0.5,0.5"
+ wall height=(mm)0.3 profile="smooth;…"
+ edge height=4.0 profile="smooth;…"
}
```
@@ -694,21 +694,35 @@ carve drops `relief_shade::RECESS_DEPTH` (0.6) of its wall (saturating at the
DE roll width) and the roll is a quarter-round of radius width, the look every
config had. `profile` is the curve as a ramp spec (absent or the identity
sentinel = the analytic curve: smoothstep for a wall, the superellipse
-quadrant for an edge), and `knobs` is cce-relief's slider triple behind it,
-which only the editor reads. The registry keys never moved — `bevel_height`
-/ `roll_height`, `bevel_profile_spec` / `roll_profile_spec`,
-`bevel_profile_knobs` / `roll_profile_knobs` — so nothing downstream of the
-registry knows. Until 2026-09-28 the six were flat on the node with the wall
-UNNAMED (`height`, `profile`, `profile_knobs`) and the edge prefixed
-(`edge_height`, `edge_profile`, `edge_knobs`), which read as one shape with an
-"edge" variant rather than two shapes; those spellings survive as aliases in
+quadrant for an edge). The registry keys never moved — `bevel_height`
+/ `roll_height`, `bevel_profile_spec` / `roll_profile_spec` — so nothing
+downstream of the registry knows. Until 2026-09-28 the keys were flat on the
+node with the wall UNNAMED (`height`, `profile`) and the edge prefixed
+(`edge_height`, `edge_profile`), which read as one shape with an "edge"
+variant rather than two shapes; those spellings survive as aliases in
`layout.rs`'s flatten table, and `prefer_relief_wall_edge` drops a flat one
whenever its node spelling is present, so a file carrying both is decided by
the node and not by line order (`the_node_spelling_of_a_relief_key_wins_over_the_flat_one`).
`cce-relief`'s Save writes the node spellings and REMOVES the flat ones
(`config::remove_config_value`), so a file migrates the first time it is
saved; `wall` and `edge` are `PROP_NODES` members so their keys land as
-properties. `layout::carve_depth_px` states the drop rule
+properties.
+
+**The editor's knobs are not a style key.** cce-relief's Shoulder / Base /
+Bias triples — the slider positions behind each profile spec — rode in the
+style block as `(bevel)`-typed keys (`relief.wall.knobs` / `edge.knobs`,
+before that `profile_knobs` / `edge_knobs`) so the editor could reopen where
+it was left: editor state beside the values that draw, and the one relief
+key nothing but the editor read. They live in that app's own
+`~/.config/cce/cce-relief/state.kdl` now, one `knobs` node per Save target
+(`shared`, a retargeted file's path, or `<path>#<key>` for a `(relief)`
+value — two materials must not seed each other's sliders; `knob_state` in
+`cce-relief.rs`). The registry keys `bevel_profile_knobs` /
+`roll_profile_knobs` and their flatten arms are gone; a config that still
+carries a knob key seeds the editor once, off the raw file, and the next
+Save takes the key off under either spelling. The `(bevel)` type and
+`parse_bevel_knobs` stay, because a `(relief)` value still carries its own
+`k=` ride-along and the data editor's preview of such a value draws it. `layout::carve_depth_px` states the drop rule
once for the tessellator's CSG features and, through `WindowInfo.relief_meta`,
the shader's free carves; `carve_depth_ratio` / `roll_height_ratio` feed the
shading twin (`Finish.carve_depth` / `roll_height`). A `(relief)` value
diff --git a/src/bin/cce-relief.rs b/src/bin/cce-relief.rs
index 16872a9..7701932 100644
--- a/src/bin/cce-relief.rs
+++ b/src/bin/cce-relief.rs
@@ -32,7 +32,16 @@
//! sampled spec to stdout; Save persists to `~/.config/cce/config.kdl`
//! (`style.surface.relief`) so every cce app starts with the material —
//! or, with `--config <path>`, to that file instead (a per-app override
-//! like cce-designer's), which also seeds the knobs/depth/width on open.
+//! like cce-designer's), which also seeds the depth/width on open.
+//!
+//! The Shoulder / Base / Bias knob triples are NOT material: they are this
+//! editor's slider positions behind the profile specs, which nothing else
+//! reads. Since 2026-09-28 they live in this app's own state file
+//! (`~/.config/cce/cce-relief/state.kdl`, one `knobs` node per Save
+//! target — see `knob_state`), not in the style block beside the values
+//! that draw. A config that still carries them (`relief.wall.knobs` /
+//! `edge.knobs`, or the older `profile_knobs` / `edge_knobs`) seeds the
+//! sliders once, and the next Save takes the keys off the file.
//!
//! `--key <dotted.key>` edits a single `(relief)` VALUE in place instead
//! (`cce_ui::relief_spec::ReliefSpec` — width/depth/wall knobs/wall
@@ -392,8 +401,8 @@ struct ProfileKnobs {
impl ProfileKnobs {
/// `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
+ /// shader's own condition). It is NOT the same as "the state file
+ /// carried saved knobs": Save remembers the knob triples 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
@@ -509,6 +518,9 @@ struct BevelPopup {
/// seeds come from it. The edge section still previews but is not part
/// of a `(relief)` value (a feature material has one wall curve).
target_key: Option<String>,
+ /// The knob state's key for this Save target (`knob_state::target_id`):
+ /// which `knobs` node of `state.kdl` remembers this window's sliders.
+ state_target: String,
/// Short label for a retargeted config ("cce-designer"), shown in the
/// title and status so it's obvious which material is being edited.
target_label: Option<String>,
@@ -1249,9 +1261,12 @@ impl BevelPopup {
/// Persist the current material to the shared config
/// (`style.surface.relief` — the same keys every app reads at startup).
/// Untouched sections write the identity sentinel (= analytic); the knob
- /// triples ride along so this editor reopens where you left it.
+ /// triples go to this app's own state file (`knob_state`) so the editor
+ /// reopens where you left it without the config carrying editor state.
fn save_to_config(&mut self) {
let p = self.config_path.to_string_lossy().into_owned();
+ // The sliders' positions, whichever target the material goes to.
+ let knobs_ok = knob_state::save(&self.state_target, self.wall.values(), self.edge.values());
// `--key` mode: the whole material folds into ONE `(relief)` value
// at that key — width, depth, the wall curve, and the knob triple
// behind it (so reopening with --key seeds these sliders). The edge
@@ -1266,13 +1281,14 @@ impl BevelPopup {
knobs: Some(self.wall.values()),
profile: self.wall.custom.then(|| self.wall.last_spec.clone()),
};
- let ok = cce_ui::config::write_config_value_typed(
- &p,
- &key,
- &spec.serialize(),
- "style",
- Some("relief"),
- );
+ let ok = knobs_ok
+ & cce_ui::config::write_config_value_typed(
+ &p,
+ &key,
+ &spec.serialize(),
+ "style",
+ Some("relief"),
+ );
self.status = if ok {
println!("saved {key} -> {p}");
format!("Saved — {key} holds this material.")
@@ -1283,18 +1299,9 @@ impl BevelPopup {
}
let depth = format!("{:.3}", self.depth_slider.inner().get_scaled_value());
let width = format!("{:.2}", self.width_slider.inner().get_scaled_value());
- let knob_str = |k: &ProfileKnobs| {
- let (s, b, c) = k.values();
- format!("{s:.3},{b:.3},{c:.3}")
- };
let w = &mut |key: &str, value: &str| {
cce_ui::config::write_config_value(&p, key, value, "style")
};
- // The knob keys carry the (bevel) type explicitly, so a config that
- // never had them gains the annotation (and its editors' previews).
- let wb = |key: &str, value: &str| {
- cce_ui::config::write_config_value_typed(&p, key, value, "style", Some("bevel"))
- };
// The pinned drop is a LENGTH: written in millimetres when the
// display metric is real (fabrication reads it straight), in logical
// px when it is only assumed; 0 = follow the width.
@@ -1316,19 +1323,23 @@ impl BevelPopup {
// save MIGRATES: the flat spellings this editor wrote until
// 2026-09-28 (`height`, `profile`, `profile_knobs` for the wall,
// `edge_*` for the edge) come off the file, or the node spelling
- // would shadow a stale line forever. `remove_config_value` is true
- // for a key that is not there, so a clean file costs nothing.
+ // would shadow a stale line forever; and the knob triples come off
+ // under either spelling, since they live in the state file now.
+ // `remove_config_value` is true for a key that is not there, so a
+ // clean file costs nothing.
let ok = height_ok
& material_ok
+ & knobs_ok
& w("style.surface.relief.depth", &depth)
& w("style.surface.relief.width", &width)
& w("style.surface.relief.wall.profile", &self.wall.last_spec)
- & w("style.surface.relief.edge.profile", &self.edge.last_spec)
- & wb("style.surface.relief.wall.knobs", &knob_str(&self.wall))
- & wb("style.surface.relief.edge.knobs", &knob_str(&self.edge));
- let migrated = ["height", "edge_height", "profile", "edge_profile", "profile_knobs", "edge_knobs"]
- .iter()
- .all(|k| cce_ui::config::remove_config_value(&p, &format!("style.surface.relief.{k}")));
+ & w("style.surface.relief.edge.profile", &self.edge.last_spec);
+ let migrated = [
+ "height", "edge_height", "profile", "edge_profile",
+ "profile_knobs", "edge_knobs", "wall.knobs", "edge.knobs",
+ ]
+ .iter()
+ .all(|k| cce_ui::config::remove_config_value(&p, &format!("style.surface.relief.{k}")));
let ok = ok & migrated;
self.status = if ok {
println!("saved {p}");
@@ -1381,12 +1392,13 @@ impl Application for BevelPopup {
});
// Seeds prefer the target file's own relief keys, falling back to
- // the DE-wide registry for anything it lacks.
- let target_json = target_label
- .is_some()
- .then(|| std::fs::read_to_string(&config_path).ok())
- .flatten()
+ // the DE-wide registry for anything it lacks. The shared config is
+ // read too (until 2026-09-28 only a retargeted one was), because a
+ // legacy knob key still in it has no registry slot to fall back to.
+ let target_json = std::fs::read_to_string(&config_path)
+ .ok()
.map(|c| cce_ui::config::parse_kdl_to_json(&c));
+ let state_target = knob_state::target_id(&config_path, &shared_path, target_key.as_deref());
let target_relief = target_json.as_ref().and_then(|v| v.pointer("/style/surface/relief").cloned());
let rel_f32 = |k: &str| {
target_relief.as_ref().and_then(|r| r.get(k)).and_then(|v| v.as_f64()).map(|f| f as f32)
@@ -1437,20 +1449,16 @@ impl Application for BevelPopup {
}
cce_ui::layout::install_wall_profile_spec(ks.profile.as_deref());
}
- let (wall_seed, edge_seed) = {
- let reg = cce_ui::layout::get_style_registry().read().unwrap();
- (
- key_spec
- .as_ref()
- .and_then(|s| s.knobs)
- .or_else(|| rel_shape_str("wall", "knobs", "profile_knobs").as_deref().and_then(parse_knobs))
- .or_else(|| reg.get_string("bevel_profile_knobs").as_deref().and_then(parse_knobs)),
- rel_shape_str("edge", "knobs", "edge_knobs")
- .as_deref()
- .and_then(parse_knobs)
- .or_else(|| reg.get_string("roll_profile_knobs").as_deref().and_then(parse_knobs)),
- )
- };
+ // The sliders reopen where the last Save of THIS target left them:
+ // the state file first, then a `(relief)` value's own `k=` ride-along,
+ // then a knob key a config still carries from before the move.
+ let (saved_wall, saved_edge) = knob_state::load(&state_target);
+ let (wall_seed, edge_seed) = (
+ saved_wall
+ .or_else(|| key_spec.as_ref().and_then(|s| s.knobs))
+ .or_else(|| rel_shape_str("wall", "knobs", "profile_knobs").as_deref().and_then(parse_knobs)),
+ saved_edge.or_else(|| rel_shape_str("edge", "knobs", "edge_knobs").as_deref().and_then(parse_knobs)),
+ );
let depth = key_spec
.as_ref()
@@ -1622,6 +1630,7 @@ impl Application for BevelPopup {
},
config_path,
target_key,
+ state_target,
target_label,
ui_context: cce_ui::context::UiContext::new(),
width: 520,
@@ -2024,3 +2033,133 @@ impl Application for BevelPopup {
fn main() {
cce_ui::engine::run::<BevelPopup>();
}
+
+/// The editor's slider positions, kept in this app's own state file —
+/// `~/.config/cce/cce-relief/state.kdl` (`$XDG_CONFIG_HOME` honoured through
+/// `cce_config_dir`) — one `knobs` node per Save target:
+///
+/// ```kdl
+/// knobs target="shared" wall=(bevel)"0.500,0.500,0.500" edge=(bevel)"0.500,0.500,0.500"
+/// knobs target="/home/me/.config/cce/cce-designer/config.kdl" wall=… edge=…
+/// knobs target="/home/me/.config/cce/config.kdl#style.surface.desktop.line_relief" wall=…
+/// ```
+///
+/// Per target, because a per-app override and the shared material are two
+/// materials with two curves each, and reopening one must not seed it with
+/// the other's sliders. The triples used to ride in the style block as
+/// `relief.wall.knobs` / `edge.knobs` (before that `profile_knobs` /
+/// `edge_knobs`), `(bevel)`-typed so the data editor could preview them —
+/// editor state beside the values that draw, and the one relief key nothing
+/// but this editor read. The `(bevel)` type and `parse_bevel_knobs` stay:
+/// a `(relief)` value still carries its own `k=` ride-along, which is what
+/// the data editor's preview of such a value draws.
+mod knob_state {
+ use cce_ui::widget::parse_bevel_knobs as parse_knobs;
+
+ pub fn path() -> std::path::PathBuf {
+ cce_ui::config::cce_config_dir().join("cce-relief").join("state.kdl")
+ }
+
+ /// Which `knobs` node a Save target owns: `shared` for the DE-wide
+ /// config.kdl, a retargeted file by its path, a `--key` value by
+ /// `<path>#<key>`.
+ pub fn target_id(config_path: &std::path::Path, shared_path: &std::path::Path, key: Option<&str>) -> String {
+ match key {
+ Some(k) => format!("{}#{k}", config_path.display()),
+ None if config_path == shared_path => "shared".to_string(),
+ None => config_path.display().to_string(),
+ }
+ }
+
+ /// The saved (wall, edge) triples for `target`, each absent when the
+ /// state file has none — or a triple that does not parse, which reads
+ /// as unsaved rather than as midpoints, so the seed chain can go on to
+ /// a config's legacy key.
+ pub fn load(target: &str) -> (Option<(f32, f32, f32)>, Option<(f32, f32, f32)>) {
+ std::fs::read_to_string(path()).ok().map_or((None, None), |c| lookup(&c, target))
+ }
+
+ /// Remember `wall` and `edge` for `target`, keeping every other target's
+ /// node. `true` when the file was written.
+ pub fn save(target: &str, wall: (f32, f32, f32), edge: (f32, f32, f32)) -> bool {
+ let p = path();
+ let current = std::fs::read_to_string(&p).unwrap_or_default();
+ let Some(next) = upsert(¤t, target, wall, edge) else {
+ return false;
+ };
+ if let Some(dir) = p.parent() {
+ let _ = std::fs::create_dir_all(dir);
+ }
+ // Temp-and-rename, so a crash mid-write leaves the old file whole.
+ let tmp = p.with_extension("kdl.tmp");
+ std::fs::write(&tmp, next).is_ok() && std::fs::rename(&tmp, &p).is_ok()
+ }
+
+ /// The (wall, edge) triples of `target`'s node in `content`.
+ pub fn lookup(content: &str, target: &str) -> (Option<(f32, f32, f32)>, Option<(f32, f32, f32)>) {
+ let Ok(doc) = content.parse::<kdl::KdlDocument>() else {
+ return (None, None);
+ };
+ let Some(node) = doc.nodes().iter().find(|n| is_target(n, target)) else {
+ return (None, None);
+ };
+ let triple = |k: &str| node.get(k).and_then(|e| e.value().as_string()).and_then(parse_knobs);
+ (triple("wall"), triple("edge"))
+ }
+
+ /// `content` with `target`'s node replaced (or added), every other node
+ /// kept as it was. `None` when `content` is not KDL — the file is this
+ /// app's own, so a corrupt one is reported by the Save rather than
+ /// silently replaced.
+ pub fn upsert(content: &str, target: &str, wall: (f32, f32, f32), edge: (f32, f32, f32)) -> Option<String> {
+ let mut doc = content.parse::<kdl::KdlDocument>().ok()?;
+ doc.nodes_mut().retain(|n| !is_target(n, target));
+ let fmt = |(s, b, c): (f32, f32, f32)| format!("{s:.3},{b:.3},{c:.3}");
+ let line = format!(
+ "knobs target={} wall=(bevel){} edge=(bevel){}\n",
+ kdl::KdlValue::String(target.to_string()),
+ kdl::KdlValue::String(fmt(wall)),
+ kdl::KdlValue::String(fmt(edge)),
+ );
+ doc.nodes_mut().push(line.parse::<kdl::KdlNode>().ok()?);
+ Some(doc.to_string())
+ }
+
+ fn is_target(node: &kdl::KdlNode, target: &str) -> bool {
+ node.name().value() == "knobs" && node.get("target").and_then(|e| e.value().as_string()) == Some(target)
+ }
+
+ #[cfg(test)]
+ mod tests {
+ use super::*;
+
+ /// A Save adds or replaces exactly its own target's node and leaves
+ /// the others alone; a lookup reads back what was saved, and a
+ /// target with no node (or a corrupt triple) reads as unsaved.
+ #[test]
+ fn knob_state_keeps_one_node_per_target() {
+ let a = (0.2, 0.7, 0.9);
+ let b = (0.5, 0.5, 0.5);
+ let c = (0.1, 0.1, 0.1);
+ let s1 = upsert("", "shared", a, b).unwrap();
+ let s2 = upsert(&s1, "/x/config.kdl", c, c).unwrap();
+ let s3 = upsert(&s2, "shared", b, a).unwrap();
+ assert_eq!(s3.matches("knobs ").count(), 2, "{s3}");
+ assert_eq!(lookup(&s3, "shared"), (Some(b), Some(a)));
+ assert_eq!(lookup(&s3, "/x/config.kdl"), (Some(c), Some(c)));
+ assert_eq!(lookup(&s3, "/y/config.kdl#some.key"), (None, None));
+ assert!(s3.contains("(bevel)\"0.200,0.700,0.900\""), "the (bevel) value format: {s3}");
+ assert_eq!(lookup("knobs target=\"shared\" wall=\"junk\"", "shared"), (None, None));
+ assert!(upsert("not kdl {{{", "shared", a, b).is_none(), "a corrupt file is refused, not replaced");
+ }
+
+ #[test]
+ fn a_target_is_the_shared_file_a_path_or_a_key() {
+ let shared = std::path::Path::new("/home/me/.config/cce/config.kdl");
+ let app = std::path::Path::new("/home/me/.config/cce/cce-designer/config.kdl");
+ assert_eq!(target_id(shared, shared, None), "shared");
+ assert_eq!(target_id(app, shared, None), app.display().to_string());
+ assert_eq!(target_id(shared, shared, Some("style.surface.desktop.line_relief")), format!("{}#style.surface.desktop.line_relief", shared.display()));
+ }
+ }
+}
diff --git a/src/config.rs b/src/config.rs
index 5d86d2c..b808e8f 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1005,7 +1005,7 @@ mod tests {
assert!(update_kdl_in_memory(&mut doc, "style.surface.relief.wall.profile", spec, "style"));
assert!(update_kdl_in_memory_typed(&mut doc, "style.surface.relief.wall.height", "0.3", "style", Some("mm")));
assert!(update_kdl_in_memory(&mut doc, "style.surface.relief.edge.profile", spec, "style"));
- assert!(update_kdl_in_memory_typed(&mut doc, "style.surface.relief.edge.knobs", "0.4,0.5,0.6", "style", Some("bevel")));
+ assert!(update_kdl_in_memory_typed(&mut doc, "style.surface.relief.edge.height", "4", "style", Some("px")));
// Migrate: the flat spellings come off, and a spelling that is not
// there reports nothing removed.
assert!(remove_kdl_in_memory(&mut doc, "style.surface.relief.profile"));
@@ -1024,7 +1024,7 @@ mod tests {
assert_eq!(relief.pointer("/wall/profile").and_then(|v| v.as_str()), Some(spec));
assert_eq!(relief.pointer("/wall/height").and_then(|v| v.as_str()), Some("0.3mm"), "{relief}");
assert_eq!(relief.pointer("/edge/profile").and_then(|v| v.as_str()), Some(spec));
- assert_eq!(relief.pointer("/edge/knobs").and_then(|v| v.as_str()), Some("0.4,0.5,0.6"));
+ assert!(relief.pointer("/edge/height").is_some(), "{relief}");
assert!(relief.get("profile").is_none() && relief.get("edge_knobs").is_none(), "{relief}");
// A whole node comes off too.
assert!(remove_kdl_in_memory(&mut doc, "style.surface.relief.edge"));
diff --git a/src/layout.rs b/src/layout.rs
index 5c102a2..e443c8b 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -252,16 +252,19 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
// ridge or trough cut into a surface — and `edge` is the
// plate's perimeter roll. Each carries `height` (a length:
// the carve's drop, the roll's rise; unset = follow the
- // width), `profile` (the ramp spec of its curve, written by
- // cce-relief, installed by reload_config) and `knobs`
- // (cce-relief's "shoulder,base,bias" slider positions behind
- // that spec — only the editor reads them). The registry keys
- // keep their old names. The flat spellings on the second
- // line of each pair are the pre-rename aliases
- // (`height` / `profile` / `profile_knobs` were the wall's,
- // `edge_*` the edge's); `prefer_relief_wall_edge` drops a
- // flat one whenever its node spelling is present, so a config
+ // width) and `profile` (the ramp spec of its curve, written
+ // by cce-relief, installed by reload_config). The registry
+ // keys keep their old names. The flat spellings on the
+ // second line of each pair are the pre-rename aliases
+ // (`height` / `profile` were the wall's, `edge_*` the
+ // edge's); `prefer_relief_wall_edge` drops a flat one
+ // whenever its node spelling is present, so a config
// carrying both is decided by the node, not by line order.
+ // cce-relief's slider knobs are NOT a style key any more
+ // (`relief.wall.knobs` / `edge.knobs`, before that
+ // `profile_knobs` / `edge_knobs`): editor state, kept in
+ // that app's own state.kdl, and read off a config only by
+ // the editor itself, as a one-time seed.
"style.surface.relief.wall.height"
| "style.surface.relief.height" => "bevel_height",
"style.surface.relief.edge.height"
@@ -270,10 +273,6 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
| "style.surface.relief.profile" => "bevel_profile_spec",
"style.surface.relief.edge.profile"
| "style.surface.relief.edge_profile" => "roll_profile_spec",
- "style.surface.relief.wall.knobs"
- | "style.surface.relief.profile_knobs" => "bevel_profile_knobs",
- "style.surface.relief.edge.knobs"
- | "style.surface.relief.edge_knobs" => "roll_profile_knobs",
"style.container.section.depth" => "section_depth",
"style.surface.param.backdrop_compression" => "param_compression",
"style.surface.param.label_layout" => "param_label_layout",
@@ -407,10 +406,8 @@ pub(crate) fn prefer_relief_wall_edge(val: &mut serde_json::Value) {
const PAIRS: &[(&str, &str, &str)] = &[
("height", "wall", "height"),
("profile", "wall", "profile"),
- ("profile_knobs", "wall", "knobs"),
("edge_height", "edge", "height"),
("edge_profile", "edge", "profile"),
- ("edge_knobs", "edge", "knobs"),
];
let Some(relief) = val.pointer_mut("/style/surface/relief").and_then(|v| v.as_object_mut()) else {
return;
@@ -6966,8 +6963,8 @@ mod tests {
let val: serde_json::Value = serde_json::json!({
"style": { "surface": { "relief": {
"width": 9.3,
- "wall": { "height": "0.3mm", "profile": "smooth;0:0,1:1", "knobs": "0.5,0.5,0.5" },
- "edge": { "height": 4.0, "profile": "smooth;0:0,1:0.9", "knobs": "0.4,0.5,0.6" }
+ "wall": { "height": "0.3mm", "profile": "smooth;0:0,1:1" },
+ "edge": { "height": 4.0, "profile": "smooth;0:0,1:0.9" }
} } }
});
let mut flat = String::new();
@@ -6982,20 +6979,20 @@ mod tests {
assert_eq!(value("roll_height").as_deref(), Some("4"), "{flat}");
assert_eq!(value("bevel_profile_spec").as_deref(), Some("smooth;0:0,1:1"), "{flat}");
assert_eq!(value("roll_profile_spec").as_deref(), Some("smooth;0:0,1:0.9"), "{flat}");
- assert_eq!(value("bevel_profile_knobs").as_deref(), Some("0.5,0.5,0.5"), "{flat}");
- assert_eq!(value("roll_profile_knobs").as_deref(), Some("0.4,0.5,0.6"), "{flat}");
// And the flat legacy spellings still land on the same keys.
let old: serde_json::Value = serde_json::json!({
"style": { "surface": { "relief": {
"height": 2.0, "edge_height": 3.0, "profile": "a", "edge_profile": "b",
- "profile_knobs": "1,1,1", "edge_knobs": "2,2,2"
+ "wall": { "knobs": "1,1,1" }, "edge_knobs": "2,2,2"
} } }
});
let mut flat = String::new();
flatten_json_to_flat_props(&old, "", &mut flat);
- for k in ["bevel_height", "roll_height", "bevel_profile_spec", "roll_profile_spec", "bevel_profile_knobs", "roll_profile_knobs"] {
+ for k in ["bevel_height", "roll_height", "bevel_profile_spec", "roll_profile_spec"] {
assert!(flat.lines().any(|l| l.starts_with(&format!("{k} = "))), "{k} missing: {flat}");
}
+ // The knobs are editor state, not style: no registry key of their own.
+ assert!(!flat.contains("profile_knobs"), "{flat}");
}
/// A config carrying both spellings of one relief key is decided by the
@@ -7009,7 +7006,7 @@ mod tests {
"depth": 0.15,
"height": 2.0, "wall": { "height": 5.0 },
"edge_profile": "old", "edge": { "profile": "new" },
- "profile_knobs": "0.1,0.1,0.1"
+ "profile": "flat-only"
} } }
});
prefer_relief_wall_edge(&mut val);
@@ -7018,7 +7015,7 @@ mod tests {
assert!(relief.get("edge_profile").is_none(), "the flat edge profile yields: {relief}");
assert_eq!(relief.pointer("/wall/height").and_then(|v| v.as_f64()), Some(5.0));
assert_eq!(relief.pointer("/edge/profile").and_then(|v| v.as_str()), Some("new"));
- assert_eq!(relief.get("profile_knobs").and_then(|v| v.as_str()), Some("0.1,0.1,0.1"), "no node spelling: kept");
+ assert_eq!(relief.get("profile").and_then(|v| v.as_str()), Some("flat-only"), "no node spelling: kept");
assert_eq!(relief.get("depth").and_then(|v| v.as_f64()), Some(0.15));
let mut flat = String::new();
flatten_json_to_flat_props(&val, "", &mut flat);
diff --git a/src/widget/input/bevel_preview.rs b/src/widget/input/bevel_preview.rs
index 8472e23..9010690 100644
--- a/src/widget/input/bevel_preview.rs
+++ b/src/widget/input/bevel_preview.rs
@@ -27,8 +27,9 @@ pub fn bevel_ease(shoulder: f32, base: f32, bias: f32, v: f32) -> f32 {
(num / den).clamp(0.0, 1.0)
}
-/// Parse a "shoulder,base,bias" knob triple (the `(bevel)` value format, and
-/// what cce-relief persists as `relief.wall.knobs` / `relief.edge.knobs`).
+/// Parse a "shoulder,base,bias" knob triple (the `(bevel)` value format: a
+/// `(relief)` value's `k=` ride-along, and what cce-relief keeps per target
+/// in its own state.kdl).
pub fn parse_bevel_knobs(s: &str) -> Option<(f32, f32, f32)> {
let mut it = s.split(',').map(|p| p.trim().parse::<f32>());
match (it.next(), it.next(), it.next()) {