graphic design tool
git clone https://git.lucas.co/cce-designer.git
refactor: a page has no Orientation row
A page's orientation is its Width and Height: a paper preset is written
portrait, and a landscape sheet is the two typed the other way round. A
save from before is migrated with its sheet turned as its Orientation
said, and the row is dropped from every page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 27 +++++++++++++------------
nodes/page.json | 5 ++---
src/app.rs | 11 ++++++-----
src/main.rs | 61 +++++++++++++++++++++++++++------------------------------
src/page.rs | 55 +++++++++++++++++++++++++--------------------------
src/window.rs | 2 +-
6 files changed, 79 insertions(+), 82 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 9db32d4..c661743 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1974,21 +1974,22 @@ slider clamps, and no one range holds both 0.25 inches and 1920 pixels.
**The size IS Width and Height, and a preset writes them** (since
2026-09-30). `page_node_frame` reads Width by Height in Units and nothing
-else; Preset, Orientation and Units are rows that SET those two when they
-are picked (`page::follow_page_rows`, called by the params pane's
-write-back and MCP's `set_param`, and what it overwrites is part of the
-undo step). Preset writes its size in the page's Units — the four paper
-sizes turned by Orientation, the three raster ones (`HD`, `4K`, `Square`)
-as they lie, Orientation set to say which way; Orientation swaps the two
-when they lie the other way; Units converts them, so the sheet keeps its
-size. There is no `Custom` preset: until then Width and Height were read
-only under it, a second way to say the size that the presets could not
-share. Preset names what was last picked, and a size typed in afterwards
-is the size. A save from before is carried over ONCE in the template merge
+else; Preset and Units are rows that SET those two when they are picked
+(`page::follow_page_rows`, called by the params pane's write-back and
+MCP's `set_param`, and what it overwrites is part of the undo step).
+Preset writes its size in the page's Units — the four paper sizes
+portrait, the three raster ones (`HD`, `4K`, `Square`) as they lie; Units
+converts them, so the sheet keeps its size. There is no `Custom` preset
+(until then Width and Height were read only under it, a second way to say
+the size that the presets could not share) and no Orientation row: a
+landscape sheet is Width and Height typed the other way round. Preset
+names what was last picked, and a size typed in afterwards is the size. A
+save from before is carried over ONCE in the template merge
(`page::migrate_preset_rows`), recognised by the Width row's `show_when`
still reading `Preset == Custom`, which the merge then replaces: a named
-preset is written into Width and Height as it was drawn, and a Custom page
-keeps its size and names Letter. `picking_a_page_preset_writes_its_size`
+preset is written into Width and Height, a sheet turned as its old
+Orientation row said, and a Custom page keeps its size and names Letter.
+The Orientation row is dropped from every page. `picking_a_page_preset_writes_its_size`
is the test.
**`page_shape`** draws a rectangle (with a corner radius), an ellipse, a
diff --git a/nodes/page.json b/nodes/page.json
index bf0a354..da8c5d2 100644
--- a/nodes/page.json
+++ b/nodes/page.json
@@ -4,11 +4,10 @@
"inputs": 0,
"outputs": 1,
"params": [
- { "name": "Preset", "type": "choice:Letter,A4,Legal,Tabloid,HD,4K,Square", "default": "Letter", "description": "Picking one writes its size into Width and Height, in Units: Letter, A4, Legal and Tabloid are paper sizes, HD, 4K and Square pixel sizes. Shows the last one picked." },
+ { "name": "Preset", "type": "choice:Letter,A4,Legal,Tabloid,HD,4K,Square", "default": "Letter", "description": "Picking one writes its size into Width and Height, in Units: Letter, A4, Legal and Tabloid as portrait sheets, HD, 4K and Square as pixel sizes. Shows the last one picked." },
{ "name": "Units", "type": "choice:Inches,Millimetres,Centimetres,Pixels", "default": "Inches", "description": "Unit Width and Height, and every length on the nodes drawing onto this page, are written in. Changing it converts Width and Height, so the page keeps its size." },
- { "name": "Width", "type": "float", "default": "8.5", "description": "Page width in Units; with Height, the page's size. A preset writes it, and any other width can be typed. In Pixels its physical size depends on Resolution." },
+ { "name": "Width", "type": "float", "default": "8.5", "description": "Page width in Units; with Height, the page's size. A preset writes it, and any other width can be typed; swap it with Height for landscape. In Pixels its physical size depends on Resolution." },
{ "name": "Height", "type": "float", "default": "11.0", "description": "Page height in Units; with Width, the page's size. A preset writes it, and any other height can be typed. In Pixels its physical size depends on Resolution." },
- { "name": "Orientation", "type": "choice:Portrait,Landscape", "default": "Portrait", "description": "Portrait makes the page taller than wide, Landscape wider than tall, by swapping Width and Height. A paper preset is picked in this orientation." },
{ "name": "Resolution", "type": "spinbox", "default": "300", "min": 18, "max": 1200, "step": 6, "description": "Pixels per inch (DPI). More pixels for a paper size; for pixel presets it sets the physical size. Stored in exported PNGs so prints come out at size." },
{ "name": "Color", "type": "float3", "default": "1.00:1.00:1.00", "min": 0.0, "max": 1.0, "description": "Background colour the whole page is filled with before anything is drawn on it." },
{ "name": "Opacity", "type": "slider", "default": "1.00", "min": 0.0, "max": 1.0, "step": 0.01, "description": "Opacity of the background fill, 0 to 1. At 0 the page is clear and only what is drawn on it shows; clear areas are see-through in the viewport and PNG." },
diff --git a/src/app.rs b/src/app.rs
index 1ebe810..5f284e9 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1202,9 +1202,10 @@ pub fn merge_template_defs(root: &mut FsNode, templates: &[NodeTemplate]) {
if node.node_type == "camera" {
node.params.retain(|p| p.name != "Square Aspect" && p.name != "Show Camera Pivot");
}
- // A page's size is its Width and Height now, and Preset has no
- // Custom: a save from before is carried over once, ahead of the
- // merge that would take the old rows' conditions away.
+ // A page's size is its Width and Height now, Preset has no Custom
+ // and there is no Orientation row: a save from before is carried
+ // over once, ahead of the merge that would take the old rows'
+ // conditions away.
if node.node_type == "page" {
crate::page::migrate_preset_rows(node);
}
@@ -4097,11 +4098,11 @@ impl State {
}
}
- // A page's Preset, Orientation and Units set its Width
+ // A page's Preset and Units set its Width
// and Height; what they overwrite is part of the step.
let mut followed = false;
if child.node_type == "page" {
- let setters: Vec<ParamDef> = was.iter().filter(|w| matches!(w.name.as_str(), "Preset" | "Orientation" | "Units")).cloned().collect();
+ let setters: Vec<ParamDef> = was.iter().filter(|w| matches!(w.name.as_str(), "Preset" | "Units")).cloned().collect();
for w in setters {
for r in crate::page::follow_page_rows(child, &w) {
followed = true;
diff --git a/src/main.rs b/src/main.rs
index a7ea575..be1a2c8 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6553,7 +6553,6 @@ mod tests {
"page",
&[
("Preset", "Letter"),
- ("Orientation", "Portrait"),
("Resolution", "72"),
("Color", "1.00:1.00:1.00"),
],
@@ -12636,7 +12635,7 @@ mod tests {
};
let chain = |bypassed: &[&str]| {
let mut nodes = vec![
- node("p", "page1", "page", &[("Preset", "Letter"), ("Orientation", "Portrait"), ("Resolution", "72"), ("Color", "1.00:1.00:1.00")]),
+ node("p", "page1", "page", &[("Preset", "Letter"), ("Resolution", "72"), ("Color", "1.00:1.00:1.00")]),
node("g", "grid1", "page_grid", &[("Input", "page1"), ("Cell Size", "0.5"), ("Line Width", "0.02"), ("Line Color", "0.00:0.00:0.00"), ("Fill Cells", "false")]),
node("b", "border1", "page_border", &[("Input", "grid1"), ("Width", "0.1"), ("Inset", "0.25"), ("Color", "1.00:0.00:0.00")]),
node("e", "export1", "export", &[("Input", "border1")]),
@@ -16782,17 +16781,17 @@ mod tests {
assert_eq!(clear.origin, [1.0, 2.0, 3.0]);
}
- /// A page's size is its Width and Height, always: Preset has no Custom,
- /// and picking a preset WRITES its size there, in the page's Units — a
- /// sheet turned by Orientation, a raster size as it lies, with
- /// Orientation set to say which way. Orientation swaps the two, Units
- /// converts them, and a save from before carries over once.
+ /// A page's size is its Width and Height, always: Preset has no Custom
+ /// and there is no Orientation row. Picking a preset WRITES its size
+ /// there, in the page's Units (a sheet portrait, a raster size as it
+ /// lies), Units converts them, and a save from before carries over once,
+ /// a landscape sheet as it was drawn.
#[test]
fn picking_a_page_preset_writes_its_size() {
use crate::page::{follow_page_rows, migrate_preset_rows, resolve_page};
let size = |node: &FsNode| {
let row = |n: &str| node.params.iter().find(|p| p.name == n).unwrap().text().to_string();
- (row("Width"), row("Height"), row("Orientation"))
+ (row("Width"), row("Height"))
};
let pick = |node: &mut FsNode, row: &str, value: &str| -> Vec<String> {
let p = node.params.iter_mut().find(|p| p.name == row).unwrap();
@@ -16801,47 +16800,45 @@ mod tests {
follow_page_rows(node, &was).into_iter().map(|p| p.name).collect()
};
let templates_root = crate::app::load_fs_tree();
+ let templates = crate::app::flatten_node_templates(&templates_root);
let template = templates_root.children.iter().find(|t| t.node_type == "page").unwrap().clone();
let preset = template.params.iter().find(|p| p.name == "Preset").unwrap();
assert!(!preset.choice_options().iter().any(|o| o == "Custom"), "{:?}", preset.choice_options());
- for row in ["Width", "Height", "Orientation"] {
+ assert!(template.params.iter().all(|p| p.name != "Orientation"));
+ for row in ["Width", "Height"] {
assert!(template.params.iter().find(|p| p.name == row).unwrap().show_when.is_empty(), "{row} is always shown");
}
let mut page = template.clone();
assert_eq!(pick(&mut page, "Preset", "A4"), ["Width", "Height"], "what a pick overwrites is handed back, for undo");
- assert_eq!(size(&page), ("8.268".into(), "11.693".into(), "Portrait".into()));
- assert_eq!(pick(&mut page, "Orientation", "Landscape"), ["Width", "Height"]);
- assert_eq!(size(&page), ("11.693".into(), "8.268".into(), "Landscape".into()));
- // A sheet is picked in the orientation the row names.
- pick(&mut page, "Preset", "Letter");
- assert_eq!(size(&page), ("11.00".into(), "8.50".into(), "Landscape".into()));
+ assert_eq!(size(&page), ("8.268".into(), "11.693".into()));
+ pick(&mut page, "Preset", "Tabloid");
+ assert_eq!(size(&page), ("11.00".into(), "17.00".into()), "a sheet is written portrait");
// Units converts: the sheet keeps its size.
pick(&mut page, "Units", "Millimetres");
- assert_eq!(size(&page), ("279.40".into(), "215.90".into(), "Landscape".into()));
- // A raster size is written as it lies, in the page's unit, and
- // Orientation follows it.
- pick(&mut page, "Orientation", "Portrait");
+ assert_eq!(size(&page), ("279.40".into(), "431.80".into()));
+ // A raster size is written as it lies, in the page's unit.
pick(&mut page, "Units", "Pixels");
pick(&mut page, "Preset", "HD");
- assert_eq!(size(&page), ("1920".into(), "1080".into(), "Landscape".into()));
+ assert_eq!(size(&page), ("1920".into(), "1080".into()));
let root = image_root(vec![page.clone()]);
let img = resolve_page(&root, &root.children[0], &mut Vec::new()).unwrap();
assert_eq!((img.width, img.height), (1920, 1080));
- // Another row changes nothing; nor does turning a page already turned.
+ // Another row changes nothing.
assert!(pick(&mut page, "Resolution", "72").is_empty());
- assert!(pick(&mut page, "Orientation", "Landscape").is_empty());
// A size typed in is the size, whatever Preset still names.
pick(&mut page, "Width", "640");
let root = image_root(vec![page.clone()]);
assert_eq!(resolve_page(&root, &root.children[0], &mut Vec::new()).unwrap().width, 640);
- // A save from before: its rows carry the old conditions. A named
- // preset is written into Width and Height, turned as it was drawn;
- // a Custom one keeps its size and names Letter.
+ // A save from before: its rows carry the old conditions and an
+ // Orientation row. A named preset is written into Width and Height,
+ // turned as it was drawn; a Custom one keeps its size and names
+ // Letter; the Orientation row goes.
let old = |preset: &str, w: &str, h: &str, orientation: &str| {
let mut n = template.clone();
- for (row, v) in [("Preset", preset), ("Width", w), ("Height", h), ("Orientation", orientation)] {
+ n.params.push(crate::app::ParamDef::new("Orientation", "choice:Portrait,Landscape", orientation));
+ for (row, v) in [("Preset", preset), ("Width", w), ("Height", h)] {
let p = n.params.iter_mut().find(|p| p.name == row).unwrap();
p.set_type("text");
p.set_text(v);
@@ -16853,14 +16850,14 @@ mod tests {
};
let mut tabloid = old("Tabloid", "8.5", "11.0", "Landscape");
migrate_preset_rows(&mut tabloid);
- assert_eq!(size(&tabloid), ("17.00".into(), "11.00".into(), "Landscape".into()));
+ assert_eq!(size(&tabloid), ("17.00".into(), "11.00".into()));
+ assert!(tabloid.params.iter().all(|p| p.name != "Orientation"));
let mut custom = old("Custom", "3", "2", "Portrait");
migrate_preset_rows(&mut custom);
- assert_eq!(size(&custom), ("3".into(), "2".into(), "Portrait".into()));
+ assert_eq!(size(&custom), ("3".into(), "2".into()));
assert_eq!(custom.params.iter().find(|p| p.name == "Preset").unwrap().text(), "Letter");
// Once: the merge takes the old conditions away, so a page loaded
// a second time is left as it is.
- let templates = crate::app::flatten_node_templates(&templates_root);
let mut loaded = image_root(vec![old("Custom", "3", "2", "Portrait")]);
crate::app::merge_template_defs(&mut loaded, &templates);
assert_eq!(size(&loaded.children[0]), size(&custom));
@@ -16868,8 +16865,8 @@ mod tests {
assert_eq!(size(&loaded.children[0]), size(&custom), "a second load wrote Letter over a typed size");
let mut merged = image_root(vec![old("A4", "8.5", "11.0", "Portrait")]);
crate::app::merge_template_defs(&mut merged, &templates);
- assert_eq!(size(&merged.children[0]), ("8.268".into(), "11.693".into(), "Portrait".into()));
- assert!(merged.children[0].params.iter().all(|p| p.invalid().is_none()));
+ assert_eq!(size(&merged.children[0]), ("8.268".into(), "11.693".into()));
+ assert!(merged.children[0].params.iter().all(|p| p.invalid().is_none() && p.name != "Orientation"));
// Through MCP, and undone as one step.
let mut state = State::new(false);
@@ -16879,7 +16876,7 @@ mod tests {
state
.apply_action(crate::app::McpAction::SetParam { slot, name: "Preset".into(), value: "Tabloid".into() }, &mut redraw)
.unwrap();
- assert_eq!(size(&state.current_dir().children[slot]), ("11.00".into(), "17.00".into(), "Portrait".into()));
+ assert_eq!(size(&state.current_dir().children[slot]), ("11.00".into(), "17.00".into()));
state.run_command("undo");
assert_eq!(size(&state.current_dir().children[slot]), before, "the pick and what it wrote are one step");
}
diff --git a/src/page.rs b/src/page.rs
index 4ef1954..02760a4 100644
--- a/src/page.rs
+++ b/src/page.rs
@@ -788,8 +788,8 @@ fn toggle_of(node: &FsNode, name: &str) -> bool {
}
/// The frame a `page` node describes: Width by Height in its Units. The
-/// Preset, Orientation and Units rows do not enter into it — they set
-/// Width and Height when they are picked ([`follow_page_rows`]).
+/// Preset and Units rows do not enter into it — they set Width and Height
+/// when they are picked ([`follow_page_rows`]).
fn page_node_frame(target: &FsNode) -> PageFrame {
let unit = PageUnit::parse(&node_param_str(target, "Units", "Inches"));
let dpi = page_dpi(target);
@@ -807,8 +807,9 @@ fn page_dpi(node: &FsNode) -> f32 {
node_param_f32(node, "Resolution", 300.0).round().clamp(1.0, 2400.0)
}
-/// A preset's size in inches: a sheet turned by `landscape`, a raster
-/// size as it lies, at `dpi`.
+/// A preset's size in inches: a sheet portrait, or turned by `landscape`
+/// (which only a save from before carries), a raster size as it lies, at
+/// `dpi`.
fn preset_inches(name: &str, dpi: f32, landscape: bool) -> Option<[f32; 2]> {
if let Some([w, h]) = preset_size(name) {
return Some(if landscape { [h, w] } else { [w, h] });
@@ -819,33 +820,31 @@ fn preset_inches(name: &str, dpi: f32, landscape: bool) -> Option<[f32; 2]> {
/// What a `page` node's Width and Height become when one of the rows that
/// SET them was just changed, `was` being that row as it stood before:
///
-/// - **Preset** writes the preset's size, in the node's Units. A sheet is
-/// turned by Orientation; a raster size (HD, 4K, Square) is written as
-/// it lies, and Orientation is set to say which way that is.
-/// - **Orientation** swaps Width and Height when they lie the other way.
+/// - **Preset** writes the preset's size, in the node's Units: a sheet
+/// portrait, a raster size (HD, 4K, Square) as it lies. A landscape
+/// sheet is its Width and Height typed the other way round; there is no
+/// Orientation row.
/// - **Units** converts them from the old unit, so the sheet keeps its
/// size and only the numbers change.
///
/// A Width or Height that is an expression is left to it. Returns the rows
-/// rewritten, as they were, for undo; empty when `was` is none of the
-/// three or nothing moved. There is no Custom preset: a size typed into
-/// Width and Height is the size, and Preset names what was last picked.
+/// rewritten, as they were, for undo; empty when `was` is neither or
+/// nothing moved. There is no Custom preset: a size typed into Width and
+/// Height is the size, and Preset names what was last picked.
pub fn follow_page_rows(node: &mut FsNode, was: &crate::app::ParamDef) -> Vec<crate::app::ParamDef> {
+ follow(node, was, false)
+}
+
+fn follow(node: &mut FsNode, was: &crate::app::ParamDef, landscape: bool) -> Vec<crate::app::ParamDef> {
let unit = PageUnit::parse(&node_param_str(node, "Units", "Inches"));
let dpi = page_dpi(node);
let (w, h) = (node_param_f32(node, "Width", 8.5), node_param_f32(node, "Height", 11.0));
- let landscape = node_param_str(node, "Orientation", "Portrait").eq_ignore_ascii_case("Landscape");
- let mut orientation = None;
let (nw, nh) = match was.name.as_str() {
"Preset" => {
let preset = node_param_str(node, "Preset", "Letter");
let Some([iw, ih]) = preset_inches(&preset, dpi, landscape) else { return Vec::new() };
- if preset_size(&preset).is_none() && iw != ih {
- orientation = Some(if iw > ih { "Landscape" } else { "Portrait" });
- }
(unit.from_inches(iw, dpi), unit.from_inches(ih, dpi))
}
- "Orientation" if w != h && landscape != (w > h) => (h, w),
"Units" => {
let old = PageUnit::parse(was.text());
(unit.from_inches(old.to_inches(w, dpi), dpi), unit.from_inches(old.to_inches(h, dpi), dpi))
@@ -853,29 +852,29 @@ pub fn follow_page_rows(node: &mut FsNode, was: &crate::app::ParamDef) -> Vec<cr
_ => return Vec::new(),
};
let mut rewritten = Vec::new();
- let mut set = |name: &str, text: String| {
+ for (name, value) in [("Width", nw), ("Height", nh)] {
+ let text = row_text(unit, value);
if let Some(p) = node.params.iter_mut().find(|p| p.name == name) {
if !p.is_expr() && p.text() != text {
rewritten.push(p.clone());
p.set_text(text);
}
}
- };
- set("Width", row_text(unit, nw));
- set("Height", row_text(unit, nh));
- if let Some(o) = orientation {
- set("Orientation", o.to_string());
}
rewritten
}
/// A `page` node from before Width and Height were always the size: its
-/// named preset is written into them once, and a Custom one — whose Width
-/// and Height already were the size — names Letter. Told apart by the
-/// Width row's `show_when`, which such a save carries as `Preset ==
-/// Custom` until the template merge replaces it.
+/// named preset is written into them once, a sheet turned as its
+/// Orientation row said, and a Custom one — whose Width and Height already
+/// were the size — names Letter. Told apart by the Width row's
+/// `show_when`, which such a save carries as `Preset == Custom` until the
+/// template merge replaces it. The Orientation row itself goes, from this
+/// save and any other: a page's orientation is its Width and Height.
pub fn migrate_preset_rows(node: &mut FsNode) {
let old = node.params.iter().any(|p| p.name == "Width" && p.show_when.contains("Custom"));
+ let landscape = node_param_str(node, "Orientation", "Portrait").eq_ignore_ascii_case("Landscape");
+ node.params.retain(|p| p.name != "Orientation");
if !old {
return;
}
@@ -885,7 +884,7 @@ pub fn migrate_preset_rows(node: &mut FsNode) {
p.set_text("Letter".to_string());
}
} else {
- follow_page_rows(node, &preset);
+ follow(node, &preset, landscape);
}
}
diff --git a/src/window.rs b/src/window.rs
index fe780ab..bfe0e1d 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -367,7 +367,7 @@ impl State {
if as_expr {
p.set_expr(true);
}
- // A page's Preset, Orientation and Units set its
+ // A page's Preset and Units set its
// Width and Height, and the step holds all of it.
let followed = if child.node_type == "page" {
crate::page::follow_page_rows(child, &before)