mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Revolve: the Cap option is saved as cap, as Extrude & Bevel and Inflate save it
This commit is contained in:
@@ -200,8 +200,8 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
|
||||
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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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("<path").count(), open.matches("<path").count() + 2, "two continuous caps, each with a fill and rim stroke");
|
||||
@@ -62,7 +62,7 @@ fn smooth_revolve_exports_vector_gradients_without_rasterizing_or_editing_the_so
|
||||
vectorcraft_geom::PathData::from_bezpath(&bp),
|
||||
Appearance::basic(Paint::solid(Color::rgb(0.2, 0.55, 0.9)), Paint::None, 0.0),
|
||||
);
|
||||
n.appearance.effects.push(Effect { params: json!({"offset":20,"rotationX":-25,"capEnds":true,"segments":16}), ..fx("threeD.revolve") });
|
||||
n.appearance.effects.push(Effect { params: json!({"offset":20,"rotationX":-25,"cap":true,"segments":16}), ..fx("threeD.revolve") });
|
||||
let d = doc_with(n);
|
||||
let before = d.clone();
|
||||
let svg = export(&d, &ExportOptions::default());
|
||||
|
||||
@@ -91,15 +91,15 @@ fn controls(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
});
|
||||
number(ui, d, "offset", tl!("Offset"), (0.0, 100000.0), "", 1, Some(unit));
|
||||
ui.horizontal(|ui| {
|
||||
let mut cap = d.bool("capEnds");
|
||||
let mut cap = d.bool("cap");
|
||||
let tip = tl!("Close the circular ends of open profiles. Partial sweeps keep their cut sides open.");
|
||||
if widgets::check_tip(ui, c3("Cap"), cap, true, tip) {
|
||||
cap = !cap;
|
||||
d.fields.insert("capEnds".into(), json!(cap));
|
||||
d.fields.insert("cap".into(), json!(cap));
|
||||
}
|
||||
ui.add_space((ui.available_width() - 24.0).max(0.0));
|
||||
if reset_dot(ui, "capEnds", !cap, "true") {
|
||||
d.fields.insert("capEnds".into(), json!(true));
|
||||
if reset_dot(ui, "cap", !cap, "true") {
|
||||
d.fields.insert("cap".into(), json!(true));
|
||||
}
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
|
||||
@@ -320,24 +320,24 @@ fn cap_checkbox_previews_resets_and_respects_cancel_and_one_step_undo() {
|
||||
let state = h.app.session.doc().unwrap();
|
||||
vectorcraft_effects::revolve_art(state.doc.node(state.selection.objects[0]).unwrap()).unwrap()
|
||||
};
|
||||
assert!(h.app.ui.dialog.as_ref().unwrap().bool("capEnds"), "Cap starts on");
|
||||
assert!(h.app.ui.dialog.as_ref().unwrap().bool("cap"), "Cap starts on");
|
||||
let capped = art(&h);
|
||||
h.click(h.rect("Cap").center());
|
||||
assert!(!h.app.ui.dialog.as_ref().unwrap().bool("capEnds"));
|
||||
assert!(!h.app.ui.dialog.as_ref().unwrap().bool("cap"));
|
||||
assert_ne!(art(&h), capped);
|
||||
h.click(h.response(("revolve-reset", "capEnds")).center());
|
||||
assert!(h.app.ui.dialog.as_ref().unwrap().bool("capEnds"));
|
||||
h.click(h.response(("revolve-reset", "cap")).center());
|
||||
assert!(h.app.ui.dialog.as_ref().unwrap().bool("cap"));
|
||||
assert_eq!(art(&h), capped);
|
||||
h.click(h.rect("Cap").center());
|
||||
h.click(h.response("revolve-reset-all").center());
|
||||
assert!(h.app.ui.dialog.as_ref().unwrap().bool("capEnds"));
|
||||
assert!(h.app.ui.dialog.as_ref().unwrap().bool("cap"));
|
||||
h.click(h.rect("Cap").center());
|
||||
if confirm {
|
||||
dialogs::confirm(&mut h.app).unwrap();
|
||||
let state = h.app.session.doc().unwrap();
|
||||
assert_eq!(state.doc.node(state.selection.objects[0]).unwrap().appearance.effects[0].params["capEnds"], json!(false));
|
||||
assert_eq!(state.doc.node(state.selection.objects[0]).unwrap().appearance.effects[0].params["cap"], json!(false));
|
||||
let loaded = vectorcraft_format::load(&vectorcraft_format::save(&state.doc, false)).unwrap();
|
||||
assert_eq!(loaded.node(state.selection.objects[0]).unwrap().appearance.effects[0].params["capEnds"], json!(false));
|
||||
assert_eq!(loaded.node(state.selection.objects[0]).unwrap().appearance.effects[0].params["cap"], json!(false));
|
||||
h.app.run("edit.undo", json!({})).unwrap();
|
||||
} else {
|
||||
dialogs::cancel(&mut h.app);
|
||||
|
||||
+4
-4
@@ -1028,11 +1028,11 @@ Parameters: `angle` (degrees, 0..360, default 360), `offset` (points, 0..100000,
|
||||
`lightRotation` (degrees, -180..180, default 125.26; 0 on the object's right, counterclockwise), `lightHeight`
|
||||
(degrees, 0..90, default 30; 90 straight in front), `lightBehind` (default false), `lightSoftness` (0..100%,
|
||||
default 0), `ambient` (Ambient Light, 0..200%, default 25; an earlier `lightAzimuth` and `lightElevation` are read
|
||||
as the same light), `capEnds` (boolean, default true), and `expandVisibleOnly` (boolean, default true). A vertical-line profile needs a positive offset to have a radius.
|
||||
`capEnds` closes each circular endpoint of an open profile with one curved, uniformly shaded
|
||||
as the same light), `cap` (boolean, default true), and `expandVisibleOnly` (boolean, default true). A vertical-line profile needs a positive offset to have a radius.
|
||||
`cap` closes each circular endpoint of an open profile with one curved, uniformly shaded
|
||||
vector path. Body faces share that curved rim. Partial sweeps use sectors and leave their angular
|
||||
cut sides open. Closed or coincident-end profiles are unchanged.
|
||||
Missing `capEnds` enables caps; set it to false for open endpoints. Shaded Revolve surfaces automatically interpolate curved-surface lighting with vector gradients while preserving profile corners and flat caps. There is no separate smoothing parameter. `shade: false` disables all lighting. Visibility of intersecting surfaces is approximate.
|
||||
Missing `cap` enables caps; set it to false for open endpoints. Shaded Revolve surfaces automatically interpolate curved-surface lighting with vector gradients while preserving profile corners and flat caps. There is no separate smoothing parameter. `shade: false` disables all lighting. Visibility of intersecting surfaces is approximate.
|
||||
The dialog's Lighting tab manipulates the same `lightRotation` and `lightHeight` parameters;
|
||||
its guide visibility and tab choice are UI state, excluded from saved effect parameters.
|
||||
`expandVisibleOnly` is saved with the effect. It trims covered portions of opaque solid surfaces only
|
||||
@@ -1043,7 +1043,7 @@ See [Revolve](revolve.md) for the scope and limitations.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"effect.apply","params":{"effect":"threeD.revolve","item":null,"params":{"angle":360,"offset":20,"edge":"left","rotationX":0,"rotationY":0,"rotationZ":0}}}}
|
||||
{"name":"run_command","arguments":{"command":"effect.setParams","params":{"index":0,"item":null,"params":{"angle":180,"lightRotation":30,"capEnds":true}}}}
|
||||
{"name":"run_command","arguments":{"command":"effect.setParams","params":{"index":0,"item":null,"params":{"angle":180,"lightRotation":30,"cap":true}}}}
|
||||
{"name":"run_command","arguments":{"command":"effect.setParams","params":{"index":0,"item":null,"params":{"expandVisibleOnly":true}}}}
|
||||
{"name":"run_command","arguments":{"command":"effect.expandAppearance","params":{}}}
|
||||
```
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ SVG/PDF export evaluates filled vector faces; native saves retain the profile an
|
||||
**Object > 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,
|
||||
|
||||
Reference in New Issue
Block a user