diff --git a/crates/effects/src/catalog.rs b/crates/effects/src/catalog.rs index 81c09494b..866d9b32f 100644 --- a/crates/effects/src/catalog.rs +++ b/crates/effects/src/catalog.rs @@ -1,5 +1,6 @@ use super::{ - CLASSIC_EXTRUDE, CLASSIC_REVOLVE, CROP_MARKS, EXTRUDE, INFLATE, PATHFINDER_EFFECTS, RASTERIZE, REVOLVE, ROTATE, TRAP, Value, json, trap, + CLASSIC_EXTRUDE, CLASSIC_REVOLVE, CLASSIC_ROTATE, CROP_MARKS, EXTRUDE, INFLATE, PATHFINDER_EFFECTS, RASTERIZE, REVOLVE, ROTATE, TRAP, Value, + json, trap, }; /// Catalogue entry for one effect. @@ -217,6 +218,13 @@ pub fn effect_catalog() -> Vec { "{angle: degrees (0..360, 360; how far the profile sweeps round the axis), cap: bool (true: solid, the open ends closed; false: hollow), offset: pt (0..1000, 0; the axis's distance from the profile, a ring past 0), edge: left|right (left; the axis is at the left or the right edge of the profile's bounds), rotationX: degrees (-180..180, -21.88), rotationY: degrees (-180..180, -23.4), rotationZ: degrees (-180..180, 9.06; the defaults are the Off-Axis Front position), perspectiveAngle: degrees (0..160, 0; 0 is none, wider brings the eye nearer), surface: wireframe|none|diffuse|plastic (plastic; wireframe draws the edges of every face and fills none; none keeps the paint unlit; diffuse lights the faces softly; plastic adds a glossy highlight), lights: null or array of 1..10 objects (intensity: % 0..100, rotation: degrees -180..180, height: degrees 0..90, behind: bool; overrides the single-light fields; the lights add up; color and softness are not used), 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, 100), ambient: % (0..100, 50; Ambient Light, every face whichever way it faces), highlightIntensity: % (0..100, 60; plastic only), highlightSize: % (0..100, 90; 100 broad, 0 a small spot; plastic only), blendSteps: integer (1..256, 25; how many levels the shading and the highlight are rounded to), shadingColor: black|custom|none (black; the color the faces darken toward; none keeps the paint and adds only the highlight), shadingCustom: #rrggbb (#000000; the color shadingColor custom darkens toward)}. The profile sweeps round a vertical axis, as Revolve's does, painted with the top fill (else the top stroke); every face is drawn, back to front. The defaults are VectorCraft's own.", json!({"angle":360.0,"cap":true,"offset":0.0,"edge":"left","rotationX":-21.88,"rotationY":-23.4,"rotationZ":9.06,"perspectiveAngle":0.0,"surface":"plastic","lights":null,"lightRotation":125.26438968275465,"lightHeight":30.0,"lightBehind":false,"lightIntensity":100.0,"ambient":50.0,"highlightIntensity":60.0,"highlightSize":90.0,"blendSteps":25,"shadingColor":"black","shadingCustom":"#000000"}), ), + g( + CLASSIC_ROTATE, + "Rotate (Classic)…", + &["Effect", "3D and Materials", "3D (Classic)"], + "{rotationX: degrees (-180..180, -21.88), rotationY: degrees (-180..180, -23.4), rotationZ: degrees (-180..180, 9.06; the defaults are the Off-Axis Front position), perspectiveAngle: degrees (0..160, 0; 0 is none, wider brings the eye nearer), surface: none|diffuse (diffuse; none keeps the paint unlit; diffuse lights the art softly), lights: null or array of 1..10 objects (intensity: % 0..100, rotation: degrees -180..180, height: degrees 0..90, behind: bool; overrides the single-light fields; the lights add up; color and softness are not used), 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, 100), ambient: % (0..100, 50; Ambient Light, the art whichever way it faces), blendSteps: integer (1..256, 25; how many levels the shading is rounded to), shadingColor: black|custom|none (black; the color the art darkens toward; none keeps the paint), shadingCustom: #rrggbb (#000000; the color shadingColor custom darkens toward)}. Every fill and stroke turns as one flat plane about the center of the object's bounds, as Rotate's do, each in its own paint, and is seen from either side. The defaults are VectorCraft's own.", + json!({"rotationX":-21.88,"rotationY":-23.4,"rotationZ":9.06,"perspectiveAngle":0.0,"surface":"diffuse","lights":null,"lightRotation":125.26438968275465,"lightHeight":30.0,"lightBehind":false,"lightIntensity":100.0,"ambient":50.0,"blendSteps":25,"shadingColor":"black","shadingCustom":"#000000"}), + ), g( "convertToShape.rectangle", "Rectangle…", diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index f92515580..92ee08614 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -87,10 +87,10 @@ pub use revolve::{ CLASSIC_REVOLVE, REVOLVE, has_revolve, is_revolve, revolve_art, revolve_art_in, revolve_options, validate_revolve, validate_revolve_in, }; pub use solids::{ - CLASSIC_EXTRUDE, EXTRUDE, INFLATE, ROTATE, ROTATION_PRESETS, SHADING_COLORS, SOLIDS, SolidMesh, THREE_D_TYPES, classic_extrude_options, - classic_shading, extrude_options, has_solid, inflate_options, is_three_d, solid_art, solid_art_in, three_d_meshes, three_d_meshes_in, - three_d_meshes_of, three_d_meshes_of_in, three_d_params_as, validate_solid, validate_solid_in, validate_three_d, validate_three_d_at, - validate_three_d_at_in, validate_three_d_in, validate_three_d_params, view_options, + CLASSIC_EXTRUDE, CLASSIC_ROTATE, EXTRUDE, INFLATE, ROTATE, ROTATE_SURFACES, ROTATION_PRESETS, SHADING_COLORS, SOLIDS, SolidMesh, THREE_D_TYPES, + classic_extrude_options, classic_shading, extrude_options, has_solid, inflate_options, is_classic_three_d, is_three_d, solid_art, solid_art_in, + three_d_meshes, three_d_meshes_in, three_d_meshes_of, three_d_meshes_of_in, three_d_params_as, validate_solid, validate_solid_in, + validate_three_d, validate_three_d_at, validate_three_d_at_in, validate_three_d_in, validate_three_d_params, view_options, }; pub use stylize::{ScribbleSetting, scribble_setting_id, scribble_settings}; pub use svg_filter::SVG_FILTER; diff --git a/crates/effects/src/solids.rs b/crates/effects/src/solids.rs index 7502dad46..8d9ff9eea 100644 --- a/crates/effects/src/solids.rs +++ b/crates/effects/src/solids.rs @@ -1,4 +1,4 @@ -//! Extrude & Bevel, Inflate, Rotate and Extrude & Bevel (Classic) as live, shaded vector art, +//! Extrude & Bevel, Inflate, Rotate, Extrude & Bevel (Classic) and Rotate (Classic) as live, shaded vector art, //! shared by the canvas and every exporter. Like Revolve, the source path stays editable and the effect draws its 3D faces. use std::sync::Arc; @@ -15,7 +15,7 @@ use crate::util::{flag, num, text}; use crate::{GeomContext, apply_geometry_with, is_geometry}; pub(crate) mod classic; -pub use classic::{CLASSIC_EXTRUDE, SHADING_COLORS, classic_extrude_options, classic_shading}; +pub use classic::{CLASSIC_EXTRUDE, CLASSIC_ROTATE, ROTATE_SURFACES, SHADING_COLORS, classic_extrude_options, classic_shading}; mod source; pub(crate) use source::{resolve, resolve_for}; @@ -26,13 +26,24 @@ pub const EXTRUDE: &str = "threeD.extrude"; pub const INFLATE: &str = "threeD.inflate"; pub const ROTATE: &str = "threeD.rotate"; /// The 3D effects this module draws (Revolve has its own). -pub const SOLIDS: [&str; 4] = [EXTRUDE, INFLATE, ROTATE, CLASSIC_EXTRUDE]; +pub const SOLIDS: [&str; 5] = [EXTRUDE, INFLATE, ROTATE, CLASSIC_EXTRUDE, CLASSIC_ROTATE]; /// Is `id` one of the 3D and Materials effects (Revolve and Revolve (Classic) included)? pub fn is_three_d(id: &str) -> bool { crate::revolve::is_revolve(id) || SOLIDS.contains(&id) } +/// Is `id` one of the 3D (Classic) effects: Extrude & Bevel (Classic), Revolve (Classic) or Rotate +/// (Classic)? +pub fn is_classic_three_d(id: &str) -> bool { + id == CLASSIC_EXTRUDE || id == crate::CLASSIC_REVOLVE || id == CLASSIC_ROTATE +} + +/// Does 3D effect `id` turn the art as a flat plane: Rotate or Rotate (Classic)? +fn is_plane(id: &str) -> bool { + id == ROTATE || id == CLASSIC_ROTATE +} + /// The 3D and Materials panel's object types and the effect that draws each: (type, effect id). /// Plane keeps the art flat, which is what Rotate draws. pub const THREE_D_TYPES: [(&str, &str); 4] = [("plane", ROTATE), ("extrude", EXTRUDE), ("revolve", crate::REVOLVE), ("inflate", INFLATE)]; @@ -193,6 +204,9 @@ fn validate_solid_params(id: &str, p: &Value) -> Result<(), String> { if id == CLASSIC_EXTRUDE { classic::validate(p)?; } + if id == CLASSIC_ROTATE { + classic::validate_rotate(p)?; + } crate::lighting::validate(p)?; shadow::validate(p)?; if let Some(v) = p.get("bevel") @@ -225,7 +239,7 @@ pub fn validate_solid_in(doc: Option<&Document>, n: &Node, id: &str, p: &Value) fn validate_solid_for(doc: Option<&Document>, n: &Node, id: &str, p: &Value, item: Option) -> Result<(), String> { validate_solid_params(id, p)?; let label = label_of(id); - let n = &*source::resolve_for(doc, n, label, id == ROTATE)?; + let n = &*source::resolve_for(doc, n, label, is_plane(id))?; if is_container(n) { let shapes = members(n, label)?; let b = union_bounds(&shapes).ok_or_else(|| format!("{label} requires a group holding a shape with at least two anchors"))?; @@ -708,12 +722,12 @@ fn art_for(n: &Node, fx: &Effect, shapes: &[Profile]) -> Option(id: &str, p: &Value, shapes: &'a [Profile]) -> Result>, String> { - if id == CLASSIC_EXTRUDE { + if id == CLASSIC_EXTRUDE || id == CLASSIC_ROTATE { return Ok(None); } let (Some(settings), Some(b)) = (shadow::options(p), union_bounds(shapes)) else { return Ok(None) }; @@ -775,12 +789,12 @@ type Cast<'a> = (vectorcraft_three_d::ShadowCaster, vectorcraft_three_d::Shadow, fn surfaces_for(n: &Node, fx: &Effect, shapes: &[Profile]) -> Option>> { let b = union_bounds(shapes)?; let p = crate::merged_params(&fx.id, &fx.params); - let classic = fx.id == CLASSIC_EXTRUDE; + let classic = fx.id == CLASSIC_EXTRUDE || fx.id == CLASSIC_ROTATE; let colored = !classic && view_options(&p).has_colored_lights(); let masks = has_masks(shapes); // The paint copies of every solid together: past the allowance the source path is drawn. let mut paint_bytes = 0usize; - if fx.id == ROTATE { + if is_plane(&fx.id) { // Every part turns about the centre of the object's own bounds (a group's members about // the centre of theirs), so they stay together. let mut out = vec![]; @@ -792,7 +806,8 @@ fn surfaces_for(n: &Node, fx: &Effect, shapes: &[Profile]) -> Option, mesh: Mesh, color: [f32; 4]| keep_mesh(out, total, SolidMesh { mesh, color }, limit); - if fx.id == ROTATE { + if is_plane(&fx.id) { for s in shapes { for item in &s.items { let (paint, opacity, _) = paint_of(item, s.opacity); diff --git a/crates/effects/src/solids/classic.rs b/crates/effects/src/solids/classic.rs index ef2a39e01..919de831c 100644 --- a/crates/effects/src/solids/classic.rs +++ b/crates/effects/src/solids/classic.rs @@ -1,6 +1,6 @@ //! Extrude & Bevel (Classic): the solid Extrude & Bevel builds from a path, drawn with the classic //! surfaces (Wireframe, No Shading, Diffuse and Plastic), their highlight, blend steps and shading -//! color. +//! color. Rotate (Classic): the plane Rotate turns, drawn with the No Shading and Diffuse surfaces. use std::sync::Arc; use serde_json::Value; @@ -12,6 +12,10 @@ use vectorcraft_three_d::{Bevel, BevelShape, ClassicShading, ClassicSurface, Ext use crate::util::{flag, num, text}; pub const CLASSIC_EXTRUDE: &str = "threeD.classicExtrude"; +pub const CLASSIC_ROTATE: &str = "threeD.classicRotate"; + +/// The surfaces Rotate (Classic) takes: No Shading and Diffuse. +pub const ROTATE_SURFACES: [&str; 2] = ["none", "diffuse"]; /// The width of a Wireframe edge, in points. const WIRE: f64 = 0.5; @@ -107,6 +111,29 @@ pub fn validate(p: &Value) -> Result<(), String> { validate_classic(CLASSIC_EXTRUDE, p, &RANGES, &FLAGS) } +/// Rotate (Classic)'s numeric parameters: (key, lowest, highest, whole numbers only). +const ROTATE_RANGES: [(&str, f64, f64, bool); 9] = [ + ("rotationX", -180.0, 180.0, false), + ("rotationY", -180.0, 180.0, false), + ("rotationZ", -180.0, 180.0, false), + ("perspectiveAngle", 0.0, 160.0, false), + ("lightRotation", -180.0, 180.0, false), + ("lightHeight", 0.0, 90.0, false), + ("lightIntensity", 0.0, 100.0, false), + ("ambient", 0.0, 100.0, false), + ("blendSteps", 1.0, 256.0, true), +]; + +/// Check Rotate (Classic)'s parameters `p` (its defaults filled in) as [`validate`] checks Extrude & +/// Bevel (Classic)'s, with `surface` No Shading or Diffuse only. +pub fn validate_rotate(p: &Value) -> Result<(), String> { + validate_classic(CLASSIC_ROTATE, p, &ROTATE_RANGES, &["lightBehind"])?; + if p.get("surface").is_some_and(|v| !v.as_str().is_some_and(|s| ROTATE_SURFACES.contains(&s))) { + return Err(format!("`surface` must be one of {}", ROTATE_SURFACES.join(", "))); + } + Ok(()) +} + /// Check the parameters `p` of classic 3D effect `id` (its defaults filled in): every key is one of /// its parameters, the numbers `ranges` lists, as (key, lowest, highest, whole numbers only), are in /// range, the `flags` are true or false, and `surface`, `shadingColor` and `shadingCustom` name a diff --git a/crates/effects/src/solids/source.rs b/crates/effects/src/solids/source.rs index 66ca2c1d7..585f23bd6 100644 --- a/crates/effects/src/solids/source.rs +++ b/crates/effects/src/solids/source.rs @@ -55,7 +55,7 @@ fn needs(n: &Node) -> bool { /// a clipping path too complex to cut the art with. `label` is the effect's name. The art is cut /// for the solid effect `n` carries ([`resolve_for`]). pub(crate) fn resolve<'a>(doc: Option<&Document>, n: &'a Node, label: &str) -> Result, String> { - let every_fill = super::solid(&n.appearance.effects).is_some_and(|(_, fx)| fx.id == super::ROTATE); + let every_fill = super::solid(&n.appearance.effects).is_some_and(|(_, fx)| super::is_plane(&fx.id)); resolve_for(doc, n, label, every_fill) } diff --git a/crates/effects/src/solids/tests.rs b/crates/effects/src/solids/tests.rs index 248473f42..c960bbe67 100644 --- a/crates/effects/src/solids/tests.rs +++ b/crates/effects/src/solids/tests.rs @@ -889,3 +889,80 @@ fn classic_parameters_are_checked() { assert!(err.starts_with(says), "{p}: {err}"); } } + +/// The red square, stroked black, with Rotate (Classic) and `params`: its surfaces, the fill's +/// plane first, then the stroke's outline. +fn classic_rotate(params: Value) -> Vec { + let mut n = square_node(); + n.appearance.effects.push(fx(CLASSIC_ROTATE, params)); + solid_art(&n).unwrap().children().unwrap().iter().map(|s| (**s).clone()).collect() +} + +#[test] +fn classic_rotate_turns_every_fill_and_stroke_as_one_plane() { + let front = json!({"rotationX": 0, "rotationY": 0, "rotationZ": 0}); + let surfaces = classic_rotate(front.clone()); + assert_eq!(surfaces.len(), 2, "the fill's plane and the stroke's outline"); + assert!(surfaces.iter().all(|s| s.children().unwrap().len() == 1), "each is one flat face"); + let fill = surfaces[0].visual_bounds().unwrap(); + assert!((fill.width() - 100.0).abs() < 1.0 && (fill.height() - 100.0).abs() < 1.0, "{fill:?}"); + // Turned 60° about the vertical axis, the square shows half its width and all its height, + // about the center of its bounds. + let turned = classic_rotate(json!({"rotationX": 0, "rotationY": 60, "rotationZ": 0}))[0].visual_bounds().unwrap(); + assert!((turned.width() - 50.0).abs() < 1.0 && (turned.height() - 100.0).abs() < 1.0, "{turned:?}"); + assert!((turned.center().x - 150.0).abs() < 1.0, "{turned:?}"); + // Turned past 90°, the back of the plane is drawn. + let back = classic_rotate(json!({"rotationX": 0, "rotationY": 150, "rotationZ": 0})); + assert!(back[0].visual_bounds().is_some_and(|b| (b.width() - 100.0 * 30f64.to_radians().cos()).abs() < 1.0)); +} + +#[test] +fn classic_rotate_shades_with_no_shading_or_diffuse() { + let rgb = |p: Value| fill_rgb(&classic_rotate(p)[0].children().unwrap()[0]); + // No Shading keeps the paint, the color itself kept. + let mut n = square_node(); + n.appearance.effects.push(fx(CLASSIC_ROTATE, json!({"surface": "none"}))); + let art = solid_art(&n).unwrap(); + let AppearanceItem::Fill(f) = &faces(&art)[0].appearance.items[0] else { panic!("a fill first") }; + assert_eq!(f.paint, Paint::solid(RED)); + // Diffuse: straight on with the light in front, the face is fully lit; turned away from the + // light, it is the red darkened toward black. + let lit = json!({"rotationX": 0, "rotationY": 0, "rotationZ": 0, "lightHeight": 90}); + assert!(close3(rgb(lit), RED.to_rgb())); + let [r, g, b] = rgb(json!({"rotationX": 0, "rotationY": 70, "rotationZ": 0, "lightHeight": 90})); + assert!(r < 0.8 - 0.05 && (g * 4.0 - r).abs() < 1e-4 && (b * 8.0 - r).abs() < 1e-4, "{r} {g} {b}"); + // No light: the ambient share of the red. A custom shading color darkens toward that color; + // None keeps the paint. + let dark = rgb(json!({"lightIntensity": 0, "ambient": 20, "blendSteps": 256})); + assert!((dark[0] - 0.8 * 0.2).abs() < 0.01, "{dark:?}"); + let blue = rgb(json!({"lightIntensity": 0, "ambient": 20, "shadingColor": "custom", "shadingCustom": "#0000ff"})); + assert!(blue[2] > blue[0] / 8.0 + 0.01, "{blue:?}"); + assert!(close3(rgb(json!({"lightIntensity": 0, "shadingColor": "none"})), RED.to_rgb())); +} + +#[test] +fn classic_rotate_parameters_are_checked() { + let n = square_node(); + let check = |p: Value| validate_solid(&n, CLASSIC_ROTATE, &crate::merged_params(CLASSIC_ROTATE, &p)); + assert!(check(json!({})).is_ok()); + assert!(check(json!({"surface": "none", "shadingColor": "custom", "shadingCustom": "#12aBcd", "blendSteps": 1})).is_ok()); + for (p, says) in [ + (json!({"surface": "plastic"}), "`surface` must be one of none, diffuse"), + (json!({"surface": "wireframe"}), "`surface` must be one of none, diffuse"), + (json!({"surface": 2}), "`surface` must be one of"), + (json!({"shadingColor": "red"}), "`shadingColor` must be one of black, custom, none"), + (json!({"shadingCustom": "#fff"}), "`shadingCustom` must be a #rrggbb color"), + (json!({"rotationX": 181}), "`rotationX` must be between -180 and 180 (got 181)"), + (json!({"perspectiveAngle": 161}), "`perspectiveAngle` must be between 0 and 160 (got 161)"), + (json!({"lightIntensity": 101}), "`lightIntensity` must be between 0 and 100 (got 101)"), + (json!({"blendSteps": 0}), "`blendSteps` must be a whole number between 1 and 256 (got 0)"), + (json!({"lightBehind": 1}), "`lightBehind` must be true or false"), + (json!({"highlightSize": 50}), "unknown parameter `highlightSize`"), + (json!({"drawHiddenFaces": true}), "unknown parameter `drawHiddenFaces`"), + (json!({"shadow": true}), "unknown parameter `shadow`"), + (json!({"lights": []}), "lights must contain one to ten lights"), + ] { + let err = check(p.clone()).unwrap_err(); + assert!(err.starts_with(says), "{p}: {err}"); + } +} diff --git a/crates/engine/src/cmd/effectcmd.rs b/crates/engine/src/cmd/effectcmd.rs index 566c3cd24..657d08299 100644 --- a/crates/engine/src/cmd/effectcmd.rs +++ b/crates/engine/src/cmd/effectcmd.rs @@ -78,7 +78,7 @@ pub fn specs() -> Vec { "3D Lighting Preset", [], None, - "{index: int, preset: standard|diffuse|topLeft|right, item?: appearance item index|null (omitted: the active item, else the object's), ids?|id?: [..], target?: object|contents, aligned?: bool (index and item are rows of the first target's stack: each other target edits its equal one, as the Appearance panel does for a mixed selection)} replace an applied 3D effect's light rig and ambient with an original lighting preset; preserve shading, geometry and view; all targets must have a 3D effect at index other than Extrude & Bevel (Classic) or Revolve (Classic), which have no lighting presets; one undo step → {ids}", + "{index: int, preset: standard|diffuse|topLeft|right, item?: appearance item index|null (omitted: the active item, else the object's), ids?|id?: [..], target?: object|contents, aligned?: bool (index and item are rows of the first target's stack: each other target edits its equal one, as the Appearance panel does for a mixed selection)} replace an applied 3D effect's light rig and ambient with an original lighting preset; preserve shading, geometry and view; all targets must have a 3D effect at index other than Extrude & Bevel (Classic), Revolve (Classic) or Rotate (Classic), which have no lighting presets; one undo step → {ids}", has_doc, lighting_preset ), @@ -657,7 +657,7 @@ fn lighting_preset(s: &mut Session, p: &Value) -> Result { return Err(bad(C, format!("object {} needs a 3D effect at index {index}", id.0))); } // The classic 3D effects have no lighting presets; their lights are set in their own options. - if let Some(e) = effect.filter(|e| e.id == effects::CLASSIC_EXTRUDE || e.id == effects::CLASSIC_REVOLVE) { + if let Some(e) = effect.filter(|e| effects::is_classic_three_d(&e.id)) { let name = effects::effect_info(&e.id).map_or("This effect", |info| info.label.trim_end_matches('…')); return Err(bad(C, format!("object {}: {name} at index {index} has no lighting presets; set its lights with effect.setParams", id.0))); } diff --git a/crates/engine/src/cmd/effectcmd/lighting_presets_tests.rs b/crates/engine/src/cmd/effectcmd/lighting_presets_tests.rs index 80d13dc26..2dff0d23f 100644 --- a/crates/engine/src/cmd/effectcmd/lighting_presets_tests.rs +++ b/crates/engine/src/cmd/effectcmd/lighting_presets_tests.rs @@ -130,3 +130,17 @@ fn lighting_presets_refuse_revolve_classic() { assert_eq!(s.doc().unwrap().doc, before); assert!(!s.doc().unwrap().is_dirty()); } + +#[test] +fn lighting_presets_refuse_rotate_classic() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let id = s.execute("shape.rectangle", &json!({"x":100,"y":100,"width":80,"height":60})).unwrap()["id"].as_u64().unwrap(); + s.execute("effect.apply", &json!({"effect":"threeD.classicRotate","item":null,"params":{"lightRotation":40}})).unwrap(); + s.doc_mut().unwrap().mark_saved(); + let before = s.doc().unwrap().doc.clone(); + let err = s.execute("effect.lightingPreset", &json!({"ids":[id],"index":0,"item":null,"preset":"diffuse"})).unwrap_err().to_string(); + assert!(err.contains(&format!("object {id}: Rotate (Classic) at index 0 has no lighting presets")), "{err}"); + assert_eq!(s.doc().unwrap().doc, before); + assert!(!s.doc().unwrap().is_dirty()); +} diff --git a/crates/engine/src/cmd/effectcmd/three_d_source_tests.rs b/crates/engine/src/cmd/effectcmd/three_d_source_tests.rs index 122d937b9..104363a14 100644 --- a/crates/engine/src/cmd/effectcmd/three_d_source_tests.rs +++ b/crates/engine/src/cmd/effectcmd/three_d_source_tests.rs @@ -4,7 +4,7 @@ use vectorcraft_doc::{NodeId, NodeKind}; use crate::Session; -const TYPES: [&str; 5] = ["threeD.extrude", "threeD.classicExtrude", "threeD.inflate", "threeD.rotate", "threeD.revolve"]; +const TYPES: [&str; 6] = ["threeD.extrude", "threeD.classicExtrude", "threeD.inflate", "threeD.rotate", "threeD.classicRotate", "threeD.revolve"]; fn id_of(v: &Value) -> NodeId { NodeId(v["id"].as_u64().unwrap()) diff --git a/crates/ui-egui/src/dialogs/effect.rs b/crates/ui-egui/src/dialogs/effect.rs index fdf6eb860..caa9dfdc9 100644 --- a/crates/ui-egui/src/dialogs/effect.rs +++ b/crates/ui-egui/src/dialogs/effect.rs @@ -400,7 +400,8 @@ pub(super) fn own_choices(effect: &str, key: &str, cmyk: bool) -> Option { vec![(tl!("Wireframe"), "wireframe"), (tl!("No Shading"), "none"), (tl!("Diffuse"), "diffuse"), (tl!("Plastic"), "plastic")] } - (vectorcraft_effects::CLASSIC_EXTRUDE | vectorcraft_effects::CLASSIC_REVOLVE, "shadingColor") => { + (vectorcraft_effects::CLASSIC_ROTATE, "surface") => vec![(tl!("No Shading"), "none"), (tl!("Diffuse"), "diffuse")], + (vectorcraft_effects::CLASSIC_EXTRUDE | vectorcraft_effects::CLASSIC_REVOLVE | vectorcraft_effects::CLASSIC_ROTATE, "shadingColor") => { vec![(tl!("Black"), "black"), (tl!("Custom"), "custom"), (tl!("None"), "none")] } (vectorcraft_effects::EXTRUDE | vectorcraft_effects::CLASSIC_EXTRUDE, "bevel") => vec![ @@ -970,6 +971,8 @@ mod tests { (vectorcraft_effects::CLASSIC_REVOLVE, "edge"), (vectorcraft_effects::CLASSIC_REVOLVE, "surface"), (vectorcraft_effects::CLASSIC_REVOLVE, "shadingColor"), + (vectorcraft_effects::CLASSIC_ROTATE, "surface"), + (vectorcraft_effects::CLASSIC_ROTATE, "shadingColor"), ("warp.arc", "orientation"), ("warp.twist", "orientation"), (vectorcraft_effects::RASTERIZE, "background"), diff --git a/crates/ui-egui/src/dialogs/revolve.rs b/crates/ui-egui/src/dialogs/revolve.rs index 925a593c9..c7a306887 100644 --- a/crates/ui-egui/src/dialogs/revolve.rs +++ b/crates/ui-egui/src/dialogs/revolve.rs @@ -1,5 +1,5 @@ //! Compact controls for the 3D and Materials effects: Extrude & Bevel, Revolve, Inflate and Rotate, -//! and Extrude & Bevel (Classic) and Revolve (Classic), whose own controls are in +//! and Extrude & Bevel (Classic), Revolve (Classic) and Rotate (Classic), whose own controls are in //! `three_d_classic`. The object and the rotation and light gizmos live on the artboard. All document changes run through the effect //! dialog's existing preview/commit commands. @@ -146,11 +146,11 @@ pub(super) fn c3(s: &'static str) -> &'static str { } /// Extrude & Bevel, Inflate, Extrude & Bevel (Classic) and Revolve (Classic): Object, Rotation and -/// Lighting tabs (Rotate has no Object tab). The rotation gizmo shows on the first two, the light -/// gizmo on Lighting. +/// Lighting tabs (Rotate and Rotate (Classic) have no Object tab). The rotation gizmo shows on the +/// first two, the light gizmo on Lighting. fn solid_controls(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, id: &str) { - let classic = id == CLASSIC_EXTRUDE || id == CLASSIC_REVOLVE; - let object = id == EXTRUDE || id == INFLATE || classic; + let classic = vectorcraft_effects::is_classic_three_d(id); + let object = id == EXTRUDE || id == INFLATE || id == CLASSIC_EXTRUDE || id == CLASSIC_REVOLVE; let mut tabs = vec![]; if object { tabs.push(tl!("Object")); diff --git a/crates/ui-egui/src/dialogs/revolve/tests.rs b/crates/ui-egui/src/dialogs/revolve/tests.rs index 8a2ffb306..fd802f83f 100644 --- a/crates/ui-egui/src/dialogs/revolve/tests.rs +++ b/crates/ui-egui/src/dialogs/revolve/tests.rs @@ -422,6 +422,44 @@ fn classic_revolve_shows_its_options_and_applies_as_one_step() { assert_eq!(h.app.session.doc().unwrap().doc, before); } +/// Rotate (Classic): Position on the Rotation tab, with no Object tab, and No Shading or Diffuse +/// on the Lighting tab, without the highlight or Draw Hidden Faces; OK keeps the edit as one undo +/// step. +#[test] +fn classic_rotate_shows_its_options_and_applies_as_one_step() { + use vectorcraft_effects::CLASSIC_ROTATE; + let mut h = Harness::with("M100 100 L200 100 L200 200 L100 200 Z", CLASSIC_ROTATE, vec2(1400.0, 1000.0)); + let before = h.app.session.doc().unwrap().doc.clone(); + h.settle(); + for text in ["Rotation", "Presets", "Rotation X", "Perspective", "Off-Axis Front"] { + h.rect(text); + } + assert!(!h.texts.iter().any(|(t, _)| t == "Object" || t == "Depth" || t == "Angle"), "no Object tab"); + h.click(h.rect("Lighting").center()); + h.settle(); + for text in ["Surface", "Diffuse", "Light Intensity", "Ambient Light", "Blend Steps", "Shading Color"] { + h.rect(text); + } + assert!( + !h.texts.iter().any(|(t, _)| t == "Highlight Intensity" || t == "Highlight Size" || t == "Draw Hidden Faces"), + "no highlight and no hidden faces" + ); + assert_eq!(dialogs::revolve_gizmo::hint(h.app.ui.dialog.as_ref().unwrap()), "Drag the light, or its rings. Shift snaps to 15°."); + let d = h.app.ui.dialog.as_mut().unwrap(); + d.fields.insert("surface".into(), json!("none")); + d.fields.insert("rotationY".into(), json!(40.0)); + h.settle(); + assert_eq!(dialogs::revolve_gizmo::hint(h.app.ui.dialog.as_ref().unwrap()), "Drag values or labels. Double-click a value to type."); + dialogs::confirm(&mut h.app).unwrap(); + let state = h.app.session.doc().unwrap(); + let fx = &state.doc.node(state.selection.objects[0]).unwrap().appearance.effects[0]; + assert_eq!(fx.id, CLASSIC_ROTATE); + assert_eq!((fx.params["surface"].clone(), fx.params["rotationY"].as_f64()), (json!("none"), Some(40.0))); + assert!(fx.params.get("highlightSize").is_none() && fx.params.get("drawHiddenFaces").is_none()); + h.app.run("edit.undo", json!({})).unwrap(); + assert_eq!(h.app.session.doc().unwrap().doc, before); +} + #[test] fn extrude_bevel_options_enable_with_a_bevel_shape() { let mut h = Harness::with("M100 100 L200 100 L200 200 L100 200 Z", vectorcraft_effects::EXTRUDE, vec2(1400.0, 1000.0)); diff --git a/crates/ui-egui/src/dialogs/revolve_gizmo.rs b/crates/ui-egui/src/dialogs/revolve_gizmo.rs index 5e49e4d9e..5016eacd0 100644 --- a/crates/ui-egui/src/dialogs/revolve_gizmo.rs +++ b/crates/ui-egui/src/dialogs/revolve_gizmo.rs @@ -24,11 +24,7 @@ pub(crate) fn lighting(d: &Dialog) -> bool { /// surface? fn lit(d: &Dialog) -> bool { let id = d.str("__effect"); - if id == vectorcraft_effects::CLASSIC_EXTRUDE || id == CLASSIC_REVOLVE { - matches!(d.str("surface").as_str(), "diffuse" | "plastic") - } else { - d.bool("shade") - } + if vectorcraft_effects::is_classic_three_d(&id) { matches!(d.str("surface").as_str(), "diffuse" | "plastic") } else { d.bool("shade") } } pub(crate) fn hint(d: &Dialog) -> &'static str { diff --git a/crates/ui-egui/src/dialogs/three_d_classic.rs b/crates/ui-egui/src/dialogs/three_d_classic.rs index 6c03163f0..a3857c914 100644 --- a/crates/ui-egui/src/dialogs/three_d_classic.rs +++ b/crates/ui-egui/src/dialogs/three_d_classic.rs @@ -1,11 +1,12 @@ -//! The own controls of Extrude & Bevel (Classic) and Revolve (Classic) in the compact 3D dialog: the -//! extrude and bevel options, or the revolve options, on the Object tab, and the surface with its -//! lights, highlight, blend steps and shading color on the Lighting tab. Position (presets, -//! rotation, perspective) is the Rotation tab every 3D effect shares. +//! The own controls of Extrude & Bevel (Classic), Revolve (Classic) and Rotate (Classic) in the +//! compact 3D dialog: the extrude and bevel options, or the revolve options, on the Object tab +//! (Rotate (Classic) has none), and the surface with its lights, highlight, blend steps and shading +//! color on the Lighting tab. Position (presets, rotation, perspective) is the Rotation tab every 3D +//! effect shares. use serde_json::{Value, json}; use vectorcraft_doc::Unit; -use vectorcraft_effects::{CLASSIC_EXTRUDE, CLASSIC_REVOLVE}; +use vectorcraft_effects::{CLASSIC_EXTRUDE, CLASSIC_REVOLVE, CLASSIC_ROTATE}; use super::revolve::{c3, choice, heading, label, light_list, light_record, number, reset_dot, selected_light, store_selected, toggle}; use crate::state::Dialog; @@ -40,10 +41,13 @@ pub(super) fn revolve_controls(ui: &mut egui::Ui, d: &mut Dialog, unit: Unit) { } /// The surface, then for Diffuse and Plastic the lights, Ambient Light, the highlight (Plastic -/// only), Blend Steps and Shading Color; Draw Hidden Faces last, for Extrude & Bevel (Classic) -/// (Revolve (Classic) draws every face). +/// only, and not for Rotate (Classic), which has no Plastic surface), Blend Steps and Shading Color; +/// Draw Hidden Faces last, for Extrude & Bevel (Classic) (Revolve (Classic) draws every face, and +/// Rotate (Classic)'s plane is seen from either side). pub(super) fn surface_controls(ui: &mut egui::Ui, d: &mut Dialog) { - choice(ui, d, "surface", tl!("Surface"), &options("surface")); + let id = d.str("__effect"); + let effect = if id == CLASSIC_ROTATE { CLASSIC_ROTATE } else { CLASSIC_EXTRUDE }; + choice(ui, d, "surface", tl!("Surface"), &options_of(effect, "surface")); let surface = d.str("surface"); ui.add_enabled_ui(surface == "diffuse" || surface == "plastic", |ui| { light_list(ui, d); @@ -57,24 +61,31 @@ pub(super) fn surface_controls(ui: &mut egui::Ui, d: &mut Dialog) { store_selected(d, &selected); } number(ui, d, "ambient", tl!("Ambient Light"), (0.0, 100.0), "%", 1, None); - ui.add_enabled_ui(surface == "plastic", |ui| { - number(ui, d, "highlightIntensity", tl!("Highlight Intensity"), (0.0, 100.0), "%", 1, None); - number(ui, d, "highlightSize", tl!("Highlight Size"), (0.0, 100.0), "%", 1, None); - }); + if id != CLASSIC_ROTATE { + ui.add_enabled_ui(surface == "plastic", |ui| { + number(ui, d, "highlightIntensity", tl!("Highlight Intensity"), (0.0, 100.0), "%", 1, None); + number(ui, d, "highlightSize", tl!("Highlight Size"), (0.0, 100.0), "%", 1, None); + }); + } number(ui, d, "blendSteps", tl!("Blend Steps"), (1.0, 256.0), "", 0, None); - choice(ui, d, "shadingColor", tl!("Shading Color"), &options("shadingColor")); + choice(ui, d, "shadingColor", tl!("Shading Color"), &options_of(effect, "shadingColor")); if d.str("shadingColor") == "custom" { shading_custom(ui, d); } }); - if d.str("__effect") == CLASSIC_EXTRUDE { + if id == CLASSIC_EXTRUDE { toggle(ui, d, "drawHiddenFaces", tl!("Draw Hidden Faces"), false); } } /// The values of Extrude & Bevel (Classic)'s `key` as the dropdowns list them, (value, label). fn options(key: &str) -> Vec<(Value, &'static str)> { - super::effect::own_choices(CLASSIC_EXTRUDE, key, false).unwrap_or_default().into_iter().map(|(label, value)| (json!(value), label)).collect() + options_of(CLASSIC_EXTRUDE, key) +} + +/// The values of classic 3D effect `effect`'s `key` as the dropdowns list them, (value, label). +fn options_of(effect: &str, key: &str) -> Vec<(Value, &'static str)> { + super::effect::own_choices(effect, key, false).unwrap_or_default().into_iter().map(|(label, value)| (json!(value), label)).collect() } /// The custom shading color: a color button, its hex value and its reset dot. diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 9e1883d94..73159ade1 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -1120,6 +1120,7 @@ font-technology Variable Променливи 3D (Classic) 3D (класически) Extrude & Bevel (Classic)… Избутване и фасетка (класически)… Revolve (Classic)… Завъртане (класическо)… + Rotate (Classic)… Ротация (класическа)… Surface Повърхност Wireframe Телена рамка No Shading Без засенчване diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 9c769fbf7..fe6848577 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -484,6 +484,7 @@ 3D (Classic) 3D (klasično) Extrude & Bevel (Classic)… Istiskivanje i zakošenje (klasično)… Revolve (Classic)… Rotaciono tijelo (klasično)… + Rotate (Classic)… Rotiraj (klasično)… Surface Površina Wireframe Žičani model No Shading Bez sjenčenja diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 3b36368c6..a632b5548 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -1574,6 +1574,7 @@ antiAlias Sharp Nítid 3D (Classic) 3D (clàssic) Extrude & Bevel (Classic)… Extrudeix i bisella (clàssic)… Revolve (Classic)… Revoluciona (clàssic)… + Rotate (Classic)… Gira (clàssic)… Surface Superfície Wireframe Filferro No Shading Sense ombra diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 03aaedff3..1e330b65f 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -493,6 +493,7 @@ 3D (Classic) 3D (klasické) Extrude & Bevel (Classic)… Vysunutí a zkosení (klasické)… Revolve (Classic)… Rotační těleso (klasické)… + Rotate (Classic)… Otočit (klasické)… Surface Povrch Wireframe Drátový model No Shading Bez stínování diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 9515a8a06..6184bfd04 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -451,6 +451,7 @@ 3D (Classic) 3D (klassisk) Extrude & Bevel (Classic)… Ekstrudér og facet (klassisk)… Revolve (Classic)… Omdrej (klassisk)… + Rotate (Classic)… Rotér (klassisk)… Surface Overflade Wireframe Trådmodel No Shading Ingen skyggelægning diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 411f6fc61..b8c3a7bcb 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -1125,6 +1125,7 @@ font-technology Variable Variabel 3D (Classic) 3D (klassisch) Extrude & Bevel (Classic)… Extrudieren und abgeflachte Kante (klassisch)… Revolve (Classic)… Kreiseln (klassisch)… + Rotate (Classic)… Drehen (klassisch)… Surface Oberfläche Wireframe Drahtmodell No Shading Keine Schattierung diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 698f7d985..d9e9dfaeb 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -1098,6 +1098,7 @@ font-technology Variable Μεταβλητές 3D (Classic) 3D (κλασικό) Extrude & Bevel (Classic)… Εξώθηση και λοξότμηση (κλασικό)… Revolve (Classic)… Περιστροφή γύρω από άξονα (κλασικό)… + Rotate (Classic)… Περιστροφή (κλασικό)… Surface Επιφάνεια Wireframe Συρματόπλεγμα No Shading Χωρίς σκίαση diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index e3432f8a8..260ba55a9 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1154,6 +1154,7 @@ font-technology Variable Variables 3D (Classic) 3D (clásico) Extrude & Bevel (Classic)… Extrusión y biselado (clásico)… Revolve (Classic)… Revolución (clásico)… + Rotate (Classic)… Rotar (clásico)… Surface Superficie Wireframe Malla No Shading Sin sombreado diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 6e05cfadb..efcbc2dfa 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -490,6 +490,7 @@ 3D (Classic) 3D (perinteinen) Extrude & Bevel (Classic)… Pursota ja viistä (perinteinen)… Revolve (Classic)… Pyöräytä (perinteinen)… + Rotate (Classic)… Kierrä (perinteinen)… Surface Pinta Wireframe Rautalanka No Shading Ei varjostusta diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 89bc73664..dbbaf6bc6 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -1149,6 +1149,7 @@ font-technology Variable Variables 3D (Classic) 3D (classique) Extrude & Bevel (Classic)… Extrusion et biseautage (classique)… Revolve (Classic)… Révolution (classique)… + Rotate (Classic)… Rotation (classique)… Surface Surface Wireframe Fil de fer No Shading Sans ombrage diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 96819e6e4..42908e32e 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -1746,6 +1746,7 @@ antiAlias Sharp शार्प 3D (Classic) 3D (क्लासिक) Extrude & Bevel (Classic)… एक्सट्रूडेड और बेवेल (क्लासिक)… Revolve (Classic)… रिवॉल्व (क्लासिक)… + Rotate (Classic)… घुमाव (क्लासिक)… Surface सतह Wireframe वायरफ़्रेम No Shading कोई शेडिंग नहीं diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 2a33bbe88..cae094bc2 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -1445,6 +1445,7 @@ font-technology Variable Változó 3D (Classic) 3D (klasszikus) Extrude & Bevel (Classic)… Kihúzás és élletörés (klasszikus)… Revolve (Classic)… Forgatás tengely körül (klasszikus)… + Rotate (Classic)… Elforgatás (klasszikus)… Surface Felület Wireframe Drótváz No Shading Nincs árnyékolás diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 150904d2b..9ffa8a82c 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -451,6 +451,7 @@ 3D (Classic) 3D (klasik) Extrude & Bevel (Classic)… Ekstrusi & bevel (klasik)… Revolve (Classic)… Revolusi (klasik)… + Rotate (Classic)… Putar (klasik)… Surface Permukaan Wireframe Kerangka kawat No Shading Tanpa pengarsiran diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 59d39342b..cae8c70a7 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -1155,6 +1155,7 @@ font-technology Variable Variabili 3D (Classic) 3D (classico) Extrude & Bevel (Classic)… Estrusione e smusso (classico)… Revolve (Classic)… Rivoluzione (classico)… + Rotate (Classic)… Ruota (classico)… Surface Superficie Wireframe Reticolo No Shading Nessuna ombreggiatura diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 34bf40c32..c3de03783 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -528,6 +528,7 @@ digits Malayalam マラヤーラム数字 3D (Classic) 3D(クラシック) Extrude & Bevel (Classic)… 押し出しとベベル(クラシック)… Revolve (Classic)… 回転体(クラシック)… + Rotate (Classic)… 回転(クラシック)… Surface 表面 Wireframe ワイヤーフレーム No Shading 陰影なし diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index d302a12c2..bbf5de74a 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -419,6 +419,7 @@ 3D (Classic) 3D (კლასიკური) Extrude & Bevel (Classic)… ამოწევა და წაკვეთა (კლასიკური)… Revolve (Classic)… ბრუნვა (კლასიკური)… + Rotate (Classic)… შემობრუნება (კლასიკური)… Surface ზედაპირი Wireframe მავთულის ჩარჩო No Shading დაჩრდილვის გარეშე diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 8e4bf10b6..ddb5f4917 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -521,6 +521,7 @@ digits Malayalam 말라얄람어 3D (Classic) 3D(클래식) Extrude & Bevel (Classic)… 돌출 및 경사(클래식)… Revolve (Classic)… 회전체(클래식)… + Rotate (Classic)… 회전(클래식)… Surface 표면 Wireframe 와이어프레임 No Shading 셰이딩 없음 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index c2cc31699..4aad90260 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -465,6 +465,7 @@ 3D (Classic) 3D (klassiek) Extrude & Bevel (Classic)… Extruderen en afschuinen (klassiek)… Revolve (Classic)… Draaien (klassiek)… + Rotate (Classic)… Roteren (klassiek)… Surface Oppervlak Wireframe Draadmodel No Shading Geen arcering diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 62c09cf98..fd0b820ee 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -452,6 +452,7 @@ 3D (Classic) 3D (klasyczne) Extrude & Bevel (Classic)… Wyciągnięcie i fazowanie (klasyczne)… Revolve (Classic)… Obrót (klasyczny)… + Rotate (Classic)… Obróć (klasyczny)… Surface Powierzchnia Wireframe Szkielet No Shading Bez cieniowania diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 1f482116a..0b24aaf31 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -1047,6 +1047,7 @@ antiAlias Sharp Nítido 3D (Classic) 3D (Clássico) Extrude & Bevel (Classic)… Extrusão e Bisel (Clássico)… Revolve (Classic)… Revolver (Clássico)… + Rotate (Classic)… Girar (Clássico)… Surface Superfície Wireframe Aramado No Shading Sem Sombreamento diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 3d39ab221..40c697020 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -1152,6 +1152,7 @@ font-technology Variable Вариативные 3D (Classic) 3D (классические) Extrude & Bevel (Classic)… Выдавливание и фаска (классическое)… Revolve (Classic)… Вращение (классическое)… + Rotate (Classic)… Поворот (классический)… Surface Поверхность Wireframe Каркас No Shading Без затенения diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index bc1f07f41..83e526aad 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -1133,6 +1133,7 @@ font-technology Variable Variabla 3D (Classic) 3D (klassisk) Extrude & Bevel (Classic)… Extrudera och fasa (klassisk)… Revolve (Classic)… Rotera runt axel (klassisk)… + Rotate (Classic)… Rotera (klassisk)… Surface Yta Wireframe Trådram No Shading Ingen skuggning diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 4a6d62861..fbddb6e9f 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -1125,6 +1125,7 @@ font-technology Variable Değişken 3D (Classic) 3D (Klasik) Extrude & Bevel (Classic)… Ekstrüzyon ve Eğim (Klasik)… Revolve (Classic)… Döndür (Klasik)… + Rotate (Classic)… Çevir (Klasik)… Surface Yüzey Wireframe Tel Kafes No Shading Gölgeleme Yok diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 41449c01a..a25059724 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1149,6 +1149,7 @@ font-technology Variable Варіативні 3D (Classic) 3D (класичні) Extrude & Bevel (Classic)… Видавлювання та фаска (класичне)… Revolve (Classic)… Обертання (класичне)… + Rotate (Classic)… Поворот (класичний)… Surface Поверхня Wireframe Каркас No Shading Без затінення diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index bc517d6f4..b44405514 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -1322,6 +1322,7 @@ font-technology Variable Biến thiên 3D (Classic) 3D (cổ điển) Extrude & Bevel (Classic)… Kéo dài & vát (cổ điển)… Revolve (Classic)… Xoay tròn (cổ điển)… + Rotate (Classic)… Xoay (cổ điển)… Surface Bề mặt Wireframe Khung dây No Shading Không tô bóng diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 5f4026254..980c11056 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1095,6 +1095,7 @@ antiAlias Sharp 锐利 3D (Classic) 3D (经典) Extrude & Bevel (Classic)… 突出与斜角 (经典)… Revolve (Classic)… 回转 (经典)… + Rotate (Classic)… 旋转 (经典)… Surface 表面 Wireframe 线框 No Shading 无底纹 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 83f374090..0637c0bac 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1119,6 +1119,7 @@ font-technology Variable 可變 3D (Classic) 3D (經典) Extrude & Bevel (Classic)… 突出與斜角 (經典)… Revolve (Classic)… 迴轉 (經典)… + Rotate (Classic)… 旋轉 (經典)… Surface 表面 Wireframe 線框 No Shading 無網底 diff --git a/crates/ui-egui/src/menus/tests.rs b/crates/ui-egui/src/menus/tests.rs index ae7e4829b..e49f4788e 100644 --- a/crates/ui-egui/src/menus/tests.rs +++ b/crates/ui-egui/src/menus/tests.rs @@ -540,6 +540,22 @@ fn classic_revolve_is_in_the_3d_classic_submenu() { assert!(at("Extrude & Bevel (Classic)…").unwrap() < at("Revolve (Classic)…").unwrap()); } +/// Effect › 3D and Materials › 3D (Classic) › Rotate (Classic)… opens its effect dialog, after +/// Revolve (Classic)…, and nowhere else. +#[test] +fn classic_rotate_is_in_the_3d_classic_submenu() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + let entries = menu_entries(&app); + let classic: Vec<_> = entries.iter().filter(|e| e.params.get("effect").and_then(Value::as_str) == Some("threeD.classicRotate")).collect(); + assert_eq!(classic.len(), 1, "listed once"); + let e = classic[0]; + assert_eq!(e.path, ["Effect", "3D and Materials", "3D (Classic)"]); + assert_eq!((e.label.as_str(), e.command.as_deref()), ("Rotate (Classic)…", Some("effect.dialog"))); + let at = |label: &str| entries.iter().position(|x| x.label == label && x.path.get(1).is_some_and(|p| p == "3D and Materials")); + assert!(at("Revolve (Classic)…").unwrap() < at("Rotate (Classic)…").unwrap()); +} + /// A document's 25 views are all listed at the bottom of the View menu, by name, and the last /// one brings back its zoom, centre and rotation after the view changed. #[test] diff --git a/docs/effects-parity.md b/docs/effects-parity.md index 49bb03781..568eaa75e 100644 --- a/docs/effects-parity.md +++ b/docs/effects-parity.md @@ -181,13 +181,13 @@ VectorCraft choices; an original approximation, never compared with another appl | Revolve | initial: live, editable profile, rotation and light gizmos, default-on Cap with continuous curved rims, smooth vector shading and round seam joins, SVG/PDF output, Expand Appearance ([revolve.md](revolve.md), #846) | | Extrude & Bevel, Inflate, Rotate | initial: live, depth, twist, taper, solid or hollow caps, five original bevel shapes, Inflate volume, both sides, twist, taper and cap, Rotate of every fill and stroke, groups and layers turned about one shared axis (#4506), contours that cross, touch or overlap merged first ([three-d.md](three-d.md)), Position presets, rotation and light gizmos, SVG/PDF output, Expand Appearance ([three-d.md](three-d.md)) | | Materials panel, lighting, ray-traced render | Window › 3D and Materials panel with the object types (Plane, Extrude, Revolve, Inflate) and the Object, Rotation and Lighting settings of the applied effect ([three-d.md](three-d.md#3d-and-materials-panel)); one to ten directional lights with per-light Color (opaque RGB, white by default), Intensity, Rotation, Height, Softness and neutral Ambient Light, Revolve and colored Inflate use smooth vector shading; Extrude & Bevel and Rotate retain planar shading; original Standard, Diffuse, Top Left and Right lighting rigs; hard-edged shadows behind or below the object from the first light ([three-d.md](three-d.md#shadows)); no materials, soft shadows or ray tracing | -| 3D (Classic): Extrude & Bevel, Revolve, Rotate | Extrude & Bevel (Classic) initial: live, Extrude Depth, Cap, five original bevel shapes with Height and Bevel Extent In or Out, Position presets and rotation gizmo, Perspective, Wireframe, No Shading, Diffuse and Plastic surfaces with one to ten lights, Light Intensity, Ambient Light, Highlight Intensity and Size, Blend Steps, Black, Custom or no Shading Color, Draw Hidden Faces ([create 3D objects](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/create-3d-objects.html)); Revolve (Classic) initial: live, Angle, Cap, Offset from the left or right edge, Position presets and rotation gizmo, Perspective, the same four surfaces, lights, highlight, Blend Steps and Shading Color, groups and layers revolved about one shared axis (same page); Rotate (Classic) and Map Art: none | +| 3D (Classic): Extrude & Bevel, Revolve, Rotate | Extrude & Bevel (Classic) initial: live, Extrude Depth, Cap, five original bevel shapes with Height and Bevel Extent In or Out, Position presets and rotation gizmo, Perspective, Wireframe, No Shading, Diffuse and Plastic surfaces with one to ten lights, Light Intensity, Ambient Light, Highlight Intensity and Size, Blend Steps, Black, Custom or no Shading Color, Draw Hidden Faces ([create 3D objects](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/create-3d-objects.html)); Revolve (Classic) initial: live, Angle, Cap, Offset from the left or right edge, Position presets and rotation gizmo, Perspective, the same four surfaces, lights, highlight, Blend Steps and Shading Color, groups and layers revolved about one shared axis (same page); Rotate (Classic) initial: live, every fill and stroke turned as one plane, Position presets and rotation gizmo, Perspective, No Shading and Diffuse surfaces with lights, Ambient Light, Blend Steps and Shading Color ([rotate objects in 3D](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/rotate-objects-in-3d.html)); Map Art: none | | Turntable (30.0 beta) | none | | Export 3D objects (OBJ, USDA, glTF) | initial: Asset Export's Export 3D Object collects an object with Extrude & Bevel, Revolve, Inflate or Rotate as a 3D asset, and Export writes its solids as a USDA (the default), USDZ, glTF or OBJ mesh with a material in each solid's paint color (`assets.add3D`, `assets.set3DFormat`, `assets.export3D`); no textures, map art or shading ([3D export help](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/export-3d-vector-artwork.html)) | The `three-d` crate (depends on `geom` and `pathops`, with no UI or GPU dependencies) builds, rotates, projects, shades and depth-sorts the surfaces; the effects crate turns them into art. What's left (materials and shadows, ray-traced rendering, -angular cut caps, exact visibility, the rest of 3D Classic (Rotate, Map Art), Turntable) is 25–45 h. +angular cut caps, exact visibility, the rest of 3D Classic (Map Art), Turntable) is 25–45 h. ## Revision history diff --git a/docs/gaps.md b/docs/gaps.md index 7a8c00135..11031acc1 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -663,7 +663,7 @@ macOS cursor delivery (#2292) is narrowed to a native host adapter using the exi Extrude & Bevel, Inflate and Rotate with Position presets ([three-d.md](three-d.md), #1407). Colored Inflate has automatic smooth vector lighting from shared membrane normals. White-light Inflate keeps its existing appearance. All four take one to ten colored directional lights, with neutral ambient applied once, a light list and independent previewing controls. Standard, Diffuse, Top Left and Right select original rigs, shared with commands; manual settings show Custom. - **Missing:** materials (base, Substance, graphics), shadows, ray-traced rendering, angular cut caps, exact visibility for intersecting and concave surfaces, the rest of 3D - Classic (Rotate, Map Art), Turntable, textures, map art and shading in 3D export. + Classic (Map Art), Turntable, textures, map art and shading in 3D export. - **Impact:** specialist; Materials is the one menu stub left. - **Estimate:** 25–45 h; plan in `plan/` before coding. diff --git a/docs/mcp.md b/docs/mcp.md index 3e2b9e935..4c6340a5a 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3078,8 +3078,8 @@ See [Revolve](revolve.md) for the scope and limitations. replaces the lighting rig and ambient of an existing Revolve, Extrude & Bevel, Inflate or Rotate effect. `item: null` addresses object-level effects; an item index addresses a fill/stroke's effects. All targets must have a 3D effect at `index`, otherwise nothing changes. Extrude & Bevel -(Classic) and Revolve (Classic) have no lighting presets: a target with either at `index` is an error, nothing -changes, and `effect.setParams` sets its lights. The operation is one undo step and preserves Shading, geometry, source paint and the view. +(Classic), Revolve (Classic) and Rotate (Classic) have no lighting presets: a target with any of them at `index` is an +error, nothing changes, and `effect.setParams` sets its lights. The operation is one undo step and preserves Shading, geometry, source paint and the view. The four original rigs use one white front light at 80% intensity. Standard keeps the existing default; Diffuse uses 100% Softness and 35% Ambient Light, Top Left uses Rotation 135 and Height @@ -3111,7 +3111,7 @@ art", and a loaded file draws the source path, as it does for a solid past its l documentation names the controls and their ranges ([3D and Materials panel options](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/3d-materials-panel-options.html)). The settings are saved in the native file; SVG and PDF exports draw the shadow, and `effect.expandAppearance` keeps -it as a path. Extrude & Bevel (Classic) and Revolve (Classic) have no shadow. See [three-d.md](three-d.md#shadows). +it as a path. Extrude & Bevel (Classic), Revolve (Classic) and Rotate (Classic) have no shadow. See [three-d.md](three-d.md#shadows). ```json {"name":"run_command","arguments":{"command":"effect.setParams","params":{"index":0,"item":null,"params":{"shadow":true,"shadowPosition":"below","shadowDistance":10,"shadowBounds":150}}}} @@ -3209,6 +3209,29 @@ changed. {"name":"run_command","arguments":{"command":"effect.apply","params":{"effect":"threeD.classicRevolve","item":null,"params":{"angle":270,"offset":10,"surface":"diffuse","blendSteps":64}}}} ``` +## Rotate (Classic) + +`effect.apply {effect: "threeD.classicRotate", params: {...}, ids?, item?}` is Effect › 3D and Materials › +3D (Classic) › Rotate (Classic)…. It turns every fill and stroke as one flat plane about the center of the object's +bounds, as `threeD.rotate` does, each in its own paint, and draws the plane from either side. Parameters: +`rotationX/Y/Z` and `perspectiveAngle` as above; `surface` (`diffuse`, the default, lights the art softly, and +`none` keeps the paint unlit); and the lights, `ambient`, `blendSteps`, `shadingColor` and `shadingCustom` as Extrude & +Bevel (Classic) takes them. It has no highlight, no `drawHiddenFaces` and no shadow. The effect also goes on a group +or layer, whose members turn about one shared center, or on a fill or stroke. The public documentation describes the +dialog's Position and Surface settings +([Rotate objects in 3D](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/rotate-objects-in-3d.html)), +the Position presets, rotations from -180 to 180 and Perspective from 0 to 160 +([Set 3D rotation position options](https://help.adobe.com/archive/en_US/illustrator/cs5/illustrator_cs5_help.pdf#page=270)), +and No Shading and Diffuse Shading as the only surfaces for an object that uses only this effect +([Surface shading options](https://help.adobe.com/archive/en_US/illustrator/cs5/illustrator_cs5_help.pdf#page=271)). +The defaults and the shading are VectorCraft's own. An unknown parameter (`highlightSize` and `drawHiddenFaces` +included), a number out of range, a flag that is not true or false, a surface other than `none` or `diffuse`, or +another shading color is refused with an error and the document is not changed. + +```json +{"name":"run_command","arguments":{"command":"effect.apply","params":{"effect":"threeD.classicRotate","item":null,"params":{"rotationX":0,"rotationY":40,"rotationZ":0,"surface":"diffuse","blendSteps":64}}}} +``` + ## Edit Colors and Recolor Artwork diff --git a/docs/three-d.md b/docs/three-d.md index 5fe3d2a89..a79e6a490 100644 --- a/docs/three-d.md +++ b/docs/three-d.md @@ -141,7 +141,7 @@ them all. The members stay editable: moving or editing one reshapes the solids. ## Clipping groups, blends and symbol instances -Every 3D effect (Extrude & Bevel, Extrude & Bevel (Classic), Inflate, Rotate, Revolve and Revolve (Classic)) also applies to a clipping +Every 3D effect (Extrude & Bevel, Extrude & Bevel (Classic), Inflate, Rotate, Rotate (Classic), Revolve and Revolve (Classic)) also applies to a clipping group, a blend or a symbol instance, alone or inside a group or layer. The help page says that effects apply to objects, groups or layers ([Apply and modify effects](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/apply-filter-effects/apply-and-modify-effects.html)), that a clipping mask hides everything outside its shape ([Clipping masks](https://helpx.adobe.com/illustrator/using/clipping-masks.html)), @@ -279,7 +279,7 @@ Our reading: command is refused with "The 3D shadow is too complex; turn Shadows off or simplify the art", and a loaded file draws the source path. - Canvas, SVG and PDF exports draw the shadow, the art's bounds take it in, and Expand Appearance keeps it as a - path below the faces. Extrude & Bevel (Classic) and Revolve (Classic) have no shadow. + path below the faces. Extrude & Bevel (Classic), Revolve (Classic) and Rotate (Classic) have no shadow. ## Parameters (`effect.apply`, `effect.setParams`) @@ -336,9 +336,10 @@ out, perspective). No Adobe software, assets, presets or bevel shapes were used. outline crosses. An outline of tens of thousands of points that crosses nearly every cell (a comb of 4,000 teeth across the shape) is refused because the 3D surface is too complex; a closed path of 65,536 points round a circle builds. -- **3D (Classic)**: Extrude & Bevel (Classic) and Revolve (Classic) are built, with their surface types and blend - steps. Revolve (Classic) sweeps the surface Revolve sweeps, at 64 segments for a full turn, and draws every face back - to front, so it has no Draw Hidden Faces setting. Rotate (Classic), the track cube, map art, Turntable and 3D export +- **3D (Classic)**: Extrude & Bevel (Classic), Revolve (Classic) and Rotate (Classic) are built, with their surface + types and blend steps. Revolve (Classic) sweeps the surface Revolve sweeps, at 64 segments for a full turn, and draws + every face back to front, so it has no Draw Hidden Faces setting. Rotate (Classic) turns the plane Rotate turns, seen + from either side, with the No Shading and Diffuse surfaces only. The track cube, map art, Turntable and 3D export are separate, unbuilt items. Release measurements and a repeatable CLI scene are in [3D lighting performance](3d-lighting-performance.md).