diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 61230ba7e..c32558fad 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -200,8 +200,8 @@ pub fn effect_catalog() -> Vec { REVOLVE, "Revolve…", &["Effect", "3D and Materials"], - "{angle: degrees (0..360, 360), offset: pt (0..100000, 0), edge: left|right, rotationX: degrees (0), rotationY: degrees (0), rotationZ: degrees (0), perspectiveAngle: degrees (0..160, 0; 0 is none, wider brings the eye nearer), perspective: % (0..100; the earlier form, read as the perspectiveAngle that shows the same view), segments: integer (8..128, 64), shade: bool (true), lightRotation: degrees (-180..180, 125.26; round the object on the page, 0 on its right, counterclockwise), lightHeight: degrees (0..90, 30; 0 beside the object, 90 straight in front), lightBehind: bool (false; true lights it from behind), lightIntensity: % (0..100, 80), lightSoftness: % (0..100, 0; how far round the object the light reaches), ambient: % (0..200, 25; Ambient Light), lightAzimuth: degrees (the earlier form: with lightElevation, read as the lightRotation, lightHeight and lightBehind of the same light), lightElevation: degrees (the earlier form, with lightAzimuth), capEnds: bool (true, close circular ends of open profiles; partial-sweep cut sides stay open), expandVisibleOnly: bool (true, trim covered opaque solid surfaces only on Expand Appearance)}. Shaded surfaces use smooth vector gradients automatically; intersecting profiles use approximate painter visibility.", - json!({"angle":360.0,"offset":0.0,"edge":"left","rotationX":0.0,"rotationY":0.0,"rotationZ":0.0,"perspectiveAngle":0.0,"segments":64,"shade":true,"lightRotation":125.26438968275465,"lightHeight":30.0,"lightBehind":false,"lightIntensity":80.0,"lightSoftness":0.0,"ambient":25.0,"capEnds":true,"expandVisibleOnly":true}), + "{angle: degrees (0..360, 360), offset: pt (0..100000, 0), edge: left|right, rotationX: degrees (0), rotationY: degrees (0), rotationZ: degrees (0), perspectiveAngle: degrees (0..160, 0; 0 is none, wider brings the eye nearer), perspective: % (0..100; the earlier form, read as the perspectiveAngle that shows the same view), segments: integer (8..128, 64), shade: bool (true), lightRotation: degrees (-180..180, 125.26; round the object on the page, 0 on its right, counterclockwise), lightHeight: degrees (0..90, 30; 0 beside the object, 90 straight in front), lightBehind: bool (false; true lights it from behind), lightIntensity: % (0..100, 80), lightSoftness: % (0..100, 0; how far round the object the light reaches), ambient: % (0..200, 25; Ambient Light), lightAzimuth: degrees (the earlier form: with lightElevation, read as the lightRotation, lightHeight and lightBehind of the same light), lightElevation: degrees (the earlier form, with lightAzimuth), cap: bool (true, close circular ends of open profiles; partial-sweep cut sides stay open), expandVisibleOnly: bool (true, trim covered opaque solid surfaces only on Expand Appearance)}. Shaded surfaces use smooth vector gradients automatically; intersecting profiles use approximate painter visibility.", + json!({"angle":360.0,"offset":0.0,"edge":"left","rotationX":0.0,"rotationY":0.0,"rotationZ":0.0,"perspectiveAngle":0.0,"segments":64,"shade":true,"lightRotation":125.26438968275465,"lightHeight":30.0,"lightBehind":false,"lightIntensity":80.0,"lightSoftness":0.0,"ambient":25.0,"cap":true,"expandVisibleOnly":true}), ), g( EXTRUDE, diff --git a/crates/effects/src/revolve.rs b/crates/effects/src/revolve.rs index f10958e13..79540333f 100644 --- a/crates/effects/src/revolve.rs +++ b/crates/effects/src/revolve.rs @@ -39,7 +39,7 @@ pub fn revolve_options(p: &Value) -> Revolve { ambient: num(p, "ambient", 25.0) / 100.0, shade: flag(p, "shade", true), smooth_shading: true, - cap_ends: flag(p, "capEnds", true), + cap_ends: flag(p, "cap", true), } } @@ -54,8 +54,8 @@ pub fn validate_revolve(n: &Node, p: &Value) -> Result<(), String> { if p.get("edge").is_some_and(|v| v.as_str() != Some("left") && v.as_str() != Some("right")) { return Err("Revolve edge must be left or right".into()); } - if p.get("capEnds").is_some_and(|v| v.as_bool().is_none()) { - return Err("Revolve capEnds must be a boolean".into()); + if p.get("cap").is_some_and(|v| v.as_bool().is_none()) { + return Err("Revolve cap must be a boolean".into()); } vectorcraft_three_d::revolve(&path, b, revolve_options(p)).map(|_| ()) } @@ -257,7 +257,7 @@ mod tests { crate::new_effect( REVOLVE, &json!({ - "offset":20,"rotationX":-18,"segments":24,"capEnds":true, + "offset":20,"rotationX":-18,"segments":24,"cap":true, "shade":shade,"angle":angle }), ) @@ -315,15 +315,15 @@ mod tests { let mut effect = fx(); effect.params["offset"] = json!(20); effect.params["rotationX"] = json!(-35); - effect.params.as_object_mut().unwrap().remove("capEnds"); + effect.params.as_object_mut().unwrap().remove("cap"); n.appearance.effects.push(effect); let capped = revolve_art(&n).unwrap(); - n.appearance.effects[0].params["capEnds"] = json!(false); + n.appearance.effects[0].params["cap"] = json!(false); let uncapped = revolve_art(&n).unwrap(); assert_ne!(uncapped, capped); - n.appearance.effects[0].params["capEnds"] = json!(true); + n.appearance.effects[0].params["cap"] = json!(true); assert_eq!(revolve_art(&n), Some(capped)); - n.appearance.effects[0].params["capEnds"] = json!(false); + n.appearance.effects[0].params["cap"] = json!(false); assert_eq!(revolve_art(&n), Some(uncapped)); } #[test] diff --git a/crates/engine/src/cmd/effectcmd.rs b/crates/engine/src/cmd/effectcmd.rs index 5f065453b..801491671 100644 --- a/crates/engine/src/cmd/effectcmd.rs +++ b/crates/engine/src/cmd/effectcmd.rs @@ -661,7 +661,7 @@ mod tests { let initial = node(&s, id); let source = initial.path_data().unwrap().clone(); assert!(!initial.appearance.effects[0].params.as_object().unwrap().contains_key("smoothShading")); - assert!(initial.appearance.effects[0].params["capEnds"].as_bool().unwrap()); + assert!(initial.appearance.effects[0].params["cap"].as_bool().unwrap()); let mut gradients = 0; effects_for_test(&s, id).walk(&mut |face| { gradients += face.appearance.items.iter().filter(|item| { @@ -693,22 +693,22 @@ mod tests { s.execute( "effect.apply", &json!({"effect":"threeD.revolve","ids":[id.0],"item":null, - "params":{"offset":30,"rotationX":-35,"segments":16,"expandVisibleOnly":false,"capEnds":false}}), + "params":{"offset":30,"rotationX":-35,"segments":16,"expandVisibleOnly":false,"cap":false}}), ) .unwrap(); let uncapped = node(&s, id); - assert_eq!(uncapped.appearance.effects[0].params["capEnds"], json!(false)); + assert_eq!(uncapped.appearance.effects[0].params["cap"], json!(false)); let count = |n: &vectorcraft_doc::Node| n.children().unwrap()[0].children().unwrap().len(); let open_count = count(&effects_for_test(&s, id)); - s.execute("effect.setParams", &json!({"index":0,"ids":[id.0],"item":null,"params":{"capEnds":true}})).unwrap(); + s.execute("effect.setParams", &json!({"index":0,"ids":[id.0],"item":null,"params":{"cap":true}})).unwrap(); let capped = node(&s, id); let art = effects_for_test(&s, id); assert_eq!(count(&art), open_count + 2, "each endpoint adds one continuous vector cap"); assert_eq!(capped.path_data(), Some(&source)); - assert!(s.execute("effect.setParams", &json!({"index":0,"ids":[id.0],"item":null,"params":{"capEnds":"yes"}})).is_err()); + assert!(s.execute("effect.setParams", &json!({"index":0,"ids":[id.0],"item":null,"params":{"cap":"yes"}})).is_err()); assert_eq!(node(&s, id), capped, "a bad boolean does not mutate the document"); let restored = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap(); - assert_eq!(restored.node(id).unwrap().appearance.effects[0].params["capEnds"], json!(true)); + assert_eq!(restored.node(id).unwrap().appearance.effects[0].params["cap"], json!(true)); assert_eq!(effects::revolve_art(restored.node(id).unwrap()), Some(art.clone())); let pdf = s.execute("document.serialize", &json!({"format":"pdf"})).unwrap(); assert!(vectorcraft_format::base64_decode(pdf["dataBase64"].as_str().unwrap()).unwrap().starts_with(b"%PDF")); diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs index 335c2eaf4..c9ea15851 100644 --- a/crates/pdf/src/tests.rs +++ b/crates/pdf/src/tests.rs @@ -293,7 +293,7 @@ fn smooth_revolve_exports_axial_vector_shading_without_images_and_keeps_the_sour n.appearance.effects.push(vectorcraft_doc::Effect { id: "threeD.revolve".into(), visible: true, - params: serde_json::json!({"offset":20,"rotationX":-25,"capEnds":true,"segments":16}), + params: serde_json::json!({"offset":20,"rotationX":-25,"cap":true,"segments":16}), }); add(&mut d, n); let before = d.clone(); diff --git a/crates/render/src/tests_objectfx.rs b/crates/render/src/tests_objectfx.rs index f4028302f..6a57fd1ad 100644 --- a/crates/render/src/tests_objectfx.rs +++ b/crates/render/src/tests_objectfx.rs @@ -49,10 +49,10 @@ fn revolve_caps_cover_the_opening_and_match_export_and_visible_only_expansion() n.appearance.effects = vec![fx( "threeD.revolve", json!({"offset":20,"rotationX":-35,"lightAzimuth":0, - "angle":angle,"segments":32,"capEnds":false,"expandVisibleOnly":true}), + "angle":angle,"segments":32,"cap":false,"expandVisibleOnly":true}), )]; let open = render(&doc_with(Document::new(100.0, 100.0), n.clone())); - n.appearance.effects[0].params["capEnds"] = json!(true); + n.appearance.effects[0].params["cap"] = json!(true); let d = doc_with(Document::new(100.0, 100.0), n.clone()); let live = render(&d); assert_ne!(live.pixels, open.pixels, "caps change the opening: {angle}, {opacity}"); @@ -158,7 +158,7 @@ fn revolve_rim_has_no_spikes_at_high_zoom_and_keeps_seams_covered() { use vectorcraft_doc::AppearanceItem; let path = vectorcraft_geom::PathData::from_bezpath(&vectorcraft_geom::BezPath::from_svg("M70 30 L70 70").unwrap()); let mut n = Node::path(NodeId(0), path, Appearance::basic(Paint::solid(Color::rgb(0.2, 0.55, 0.9)), Paint::None, 0.0)); - n.appearance.effects.push(fx("threeD.revolve", json!({"offset":20,"rotationX":-18,"segments":24,"capEnds":true}))); + n.appearance.effects.push(fx("threeD.revolve", json!({"offset":20,"rotationX":-18,"segments":24,"cap":true}))); let d = doc_with(Document::new(100.0, 100.0), n); let baked = vectorcraft_effects::bake_document(&d).unwrap(); let mut fills = baked.clone(); @@ -197,7 +197,7 @@ fn continuous_revolve_caps_keep_one_colour_along_the_curved_rim_at_high_zoom() { for rotation in [-18.0f64, 18.0] { for shade in [false, true] { let mut n = Node::path(NodeId(0), path.clone(), Appearance::basic(Paint::solid(Color::rgb(0.2, 0.55, 0.9)), Paint::None, 0.0)); - n.appearance.effects.push(fx("threeD.revolve", json!({"offset":20,"rotationX":rotation,"segments":8,"capEnds":true,"shade":shade}))); + n.appearance.effects.push(fx("threeD.revolve", json!({"offset":20,"rotationX":rotation,"segments":8,"cap":true,"shade":shade}))); let d = doc_with(Document::new(100.0, 100.0), n); let center_y = 50.0 + if rotation < 0.0 { -20.0 } else { 20.0 } * rotation.to_radians().cos(); let zoom = 24.0; diff --git a/crates/svg/tests/effects_export.rs b/crates/svg/tests/effects_export.rs index c994b8651..800d00e1d 100644 --- a/crates/svg/tests/effects_export.rs +++ b/crates/svg/tests/effects_export.rs @@ -25,11 +25,11 @@ fn fx(id: &str) -> Effect { fn revolve_caps_export_as_additional_vector_faces_and_leave_source_editable() { let path = vectorcraft_geom::PathData::from_bezpath(&vectorcraft_geom::BezPath::from_svg("M100 30 L100 160").unwrap()); let mut n = Node::path(NodeId(0), path, Appearance::basic(Paint::solid(Color::rgb(0.8, 0.2, 0.1)), Paint::None, 0.0)); - n.appearance.effects.push(Effect { params: json!({"offset":30,"rotationX":-35,"segments":16,"capEnds":false}), ..fx("threeD.revolve") }); + n.appearance.effects.push(Effect { params: json!({"offset":30,"rotationX":-35,"segments":16,"cap":false}), ..fx("threeD.revolve") }); let mut d = doc_with(n); let open = export(&d, &ExportOptions::default()); let id = d.layers[0].children().unwrap()[0].id; - d.node_mut(id).unwrap().appearance.effects[0].params["capEnds"] = json!(true); + d.node_mut(id).unwrap().appearance.effects[0].params["cap"] = json!(true); let before = d.clone(); let capped = export(&d, &ExportOptions::default()); assert_eq!(capped.matches(" Expand Appearance** replaces the live effect with ordinary editable vector faces. **Cap** in the shape controls closes the circular rings at the endpoints of an open profile. -It is on by default. Its saved parameter is `capEnds` (boolean, default true); missing parameters +It is on by default. Its saved parameter is `cap` (boolean, default true); missing parameters also use caps. Set it to false to keep the ends open. The reset dot and Reset all restore Cap to on. Each cap is one closed curved vector path from the endpoint ring to the rotation axis, with uniform flat shading. A full turn produces a projected circular disk (an ellipse in an orthographic view); a partial turn produces a sector. Body faces share the same curved rim,