3D (Classic): Rotate (Classic) turns flat art in 3D with the No Shading and Diffuse surfaces

Effect › 3D and Materials › 3D (Classic) › Rotate (Classic)… turns every
fill and stroke as one flat plane, as Rotate does, and shades it with the
classic No Shading or Diffuse surface.

- `effect.apply` with `threeD.classicRotate`: the Position rotation and
  Perspective, `surface` (`none` or `diffuse`), the classic lights,
  Ambient Light, Blend Steps and Shading Color. It has no highlight, no
  Draw Hidden Faces and no shadow.
- The dialog shows Position on the Rotation tab and the surface and
  lights on the Lighting tab, with no Object tab.
- Groups and layers turn about one shared center, and the effect also
  goes on a fill or stroke, as Rotate's does.
- `effect.lightingPreset` refuses it, as it refuses the other classic
  3D effects.
- Unknown parameters, numbers out of range, bad flags, a surface other
  than No Shading or Diffuse and bad shading colors are refused with an
  error.
This commit is contained in:
dotCooCoo
2026-11-11 23:44:11 -08:00
parent 5d8065e929
commit 08de741ca9
46 changed files with 313 additions and 57 deletions
+9 -1
View File
@@ -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<EffectInfo> {
"{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…",
+4 -4
View File
@@ -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;
+25 -10
View File
@@ -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<usize>) -> 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<ShadowAndSurface
/// What the shadow of the solids [`surfaces_for`] builds for effect `id` with parameters `p` on
/// `shapes` is cast from, on one plane: the faces of every solid, the shadow's settings and the
/// faces of each solid of visible art with its member's scopes and ink. `None` with the shadow
/// off, for Extrude & Bevel (Classic) or with no shapes. A stroke's solid that is not built (past
/// off, for Extrude & Bevel (Classic) and Rotate (Classic) or with no shapes. A stroke's solid that is not built (past
/// the limits) casts nothing, as it draws nothing; any other solid that is not built is an error.
/// When a member's opacity mask or blending mode applies to its solids ([`masked`]), it applies to
/// the part of the shadow they cast too ([`shadow::scoped_art`]).
fn cast_shadow<'a>(id: &str, p: &Value, shapes: &'a [Profile]) -> Result<Option<Cast<'a>>, 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<Vec<Arc<Node>>> {
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<Vec<Arc<Nod
if !crate::revolve::paint_copies_add(&mut paint_bytes, facet_copies(&f, &paint, colored), &paint) {
return None;
}
out.push((s.scopes.as_slice(), Arc::new(surface(n, f, &paint, opacity, blend, colored))));
let art = if classic { classic::surface(n, f, &paint, opacity, blend, &p) } else { surface(n, f, &paint, opacity, blend, colored) };
out.push((s.scopes.as_slice(), Arc::new(art)));
}
}
return Some(if masks { masked(n.id, out) } else { out.into_iter().map(|(_, s)| s).collect() });
@@ -1001,7 +1016,7 @@ fn meshes_for(fx: &Effect, shapes: &[Profile], total: &mut usize, limit: usize)
let view = view_options(&p);
let mut out = vec![];
let mut keep = |out: &mut Vec<SolidMesh>, 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);
+28 -1
View File
@@ -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
+1 -1
View File
@@ -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<Cow<'a, Node>, 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)
}
+77
View File
@@ -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<Node> {
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}");
}
}
+2 -2
View File
@@ -78,7 +78,7 @@ pub fn specs() -> Vec<CommandSpec> {
"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<Value> {
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)));
}
@@ -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());
}
@@ -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())
+4 -1
View File
@@ -400,7 +400,8 @@ pub(super) fn own_choices(effect: &str, key: &str, cmyk: bool) -> Option<Vec<(&'
(vectorcraft_effects::CLASSIC_EXTRUDE | vectorcraft_effects::CLASSIC_REVOLVE, "surface") => {
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"),
+5 -5
View File
@@ -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"));
@@ -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));
+1 -5
View File
@@ -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 {
+26 -15
View File
@@ -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.
+1
View File
@@ -1120,6 +1120,7 @@ font-technology Variable Променливи
3D (Classic) 3D (класически)
Extrude & Bevel (Classic)… Избутване и фасетка (класически)…
Revolve (Classic)… Завъртане (класическо)…
Rotate (Classic)… Ротация (класическа)…
Surface Повърхност
Wireframe Телена рамка
No Shading Без засенчване
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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í
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1098,6 +1098,7 @@ font-technology Variable Μεταβλητές
3D (Classic) 3D (κλασικό)
Extrude & Bevel (Classic)… Εξώθηση και λοξότμηση (κλασικό)…
Revolve (Classic)… Περιστροφή γύρω από άξονα (κλασικό)…
Rotate (Classic)… Περιστροφή (κλασικό)…
Surface Επιφάνεια
Wireframe Συρματόπλεγμα
No Shading Χωρίς σκίαση
Can't render this file because it is too large.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1746,6 +1746,7 @@ antiAlias Sharp शार्प
3D (Classic) 3D (क्लासिक)
Extrude & Bevel (Classic)… एक्सट्रूडेड और बेवेल (क्लासिक)…
Revolve (Classic)… रिवॉल्व (क्लासिक)…
Rotate (Classic)… घुमाव (क्लासिक)…
Surface सतह
Wireframe वायरफ़्रेम
No Shading कोई शेडिंग नहीं
Can't render this file because it is too large.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
451
452
453
454
455
456
457
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -528,6 +528,7 @@ digits Malayalam マラヤーラム数字
3D (Classic) 3D(クラシック)
Extrude & Bevel (Classic)… 押し出しとベベル(クラシック)…
Revolve (Classic)… 回転体(クラシック)…
Rotate (Classic)… 回転(クラシック)…
Surface 表面
Wireframe ワイヤーフレーム
No Shading 陰影なし
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -419,6 +419,7 @@
3D (Classic) 3D (კლასიკური)
Extrude & Bevel (Classic)… ამოწევა და წაკვეთა (კლასიკური)…
Revolve (Classic)… ბრუნვა (კლასიკური)…
Rotate (Classic)… შემობრუნება (კლასიკური)…
Surface ზედაპირი
Wireframe მავთულის ჩარჩო
No Shading დაჩრდილვის გარეშე
Can't render this file because it is too large.
+1
View File
@@ -521,6 +521,7 @@ digits Malayalam 말라얄람어
3D (Classic) 3D(클래식)
Extrude & Bevel (Classic)… 돌출 및 경사(클래식)…
Revolve (Classic)… 회전체(클래식)…
Rotate (Classic)… 회전(클래식)…
Surface 표면
Wireframe 와이어프레임
No Shading 셰이딩 없음
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1152,6 +1152,7 @@ font-technology Variable Вариативные
3D (Classic) 3D (классические)
Extrude & Bevel (Classic)… Выдавливание и фаска (классическое)…
Revolve (Classic)… Вращение (классическое)…
Rotate (Classic)… Поворот (классический)…
Surface Поверхность
Wireframe Каркас
No Shading Без затенения
Can't render this file because it is too large.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -1149,6 +1149,7 @@ font-technology Variable Варіативні
3D (Classic) 3D (класичні)
Extrude & Bevel (Classic)… Видавлювання та фаска (класичне)…
Revolve (Classic)… Обертання (класичне)…
Rotate (Classic)… Поворот (класичний)…
Surface Поверхня
Wireframe Каркас
No Shading Без затінення
Can't render this file because it contains an unexpected character in line 182 and column 5.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 204 and column 5.
+1
View File
@@ -1095,6 +1095,7 @@ antiAlias Sharp 锐利
3D (Classic) 3D (经典)
Extrude & Bevel (Classic)… 突出与斜角 (经典)…
Revolve (Classic)… 回转 (经典)…
Rotate (Classic)… 旋转 (经典)…
Surface 表面
Wireframe 线框
No Shading 无底纹
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1119,6 +1119,7 @@ font-technology Variable 可變
3D (Classic) 3D (經典)
Extrude & Bevel (Classic)… 突出與斜角 (經典)…
Revolve (Classic)… 迴轉 (經典)…
Rotate (Classic)… 旋轉 (經典)…
Surface 表面
Wireframe 線框
No Shading 無網底
Can't render this file because it contains an unexpected character in line 6 and column 47.
+16
View File
@@ -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]
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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.
+26 -3
View File
@@ -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
+6 -5
View File
@@ -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).