mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 12:24:54 +00:00
3D (Classic): Revolve (Classic) sweeps a profile with Angle, Cap and Offset and the classic surfaces
Effect › 3D and Materials › 3D (Classic) › Revolve (Classic)… sweeps the profile round a vertical axis, as Revolve does, and draws every face with the Wireframe, No Shading, Diffuse and Plastic surfaces of Extrude & Bevel (Classic). - `effect.apply` with `threeD.classicRevolve`: Angle (0 to 360), Cap, Offset (0 to 1000 pt) from the left or right edge of the profile's bounds, the Position rotation and Perspective, and the classic lights, Ambient Light, highlight, Blend Steps and Shading Color. - Groups and layers revolve about one shared axis, and the effect also goes on a fill or stroke, as Revolve's does. - `effect.lightingPreset` refuses it, as it refuses Extrude & Bevel (Classic). - Revolve faces carry their view-space normal, and `three_d::revolve_facets` hands them to the classic surface code. - Unknown parameters, numbers out of range and bad flags, edges, surfaces and colors are refused with an error.
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
use super::{CLASSIC_EXTRUDE, CROP_MARKS, EXTRUDE, INFLATE, PATHFINDER_EFFECTS, RASTERIZE, REVOLVE, ROTATE, TRAP, Value, json, trap};
|
||||
use super::{
|
||||
CLASSIC_EXTRUDE, CLASSIC_REVOLVE, CROP_MARKS, EXTRUDE, INFLATE, PATHFINDER_EFFECTS, RASTERIZE, REVOLVE, ROTATE, TRAP, Value, json, trap,
|
||||
};
|
||||
|
||||
/// Catalogue entry for one effect.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -81,6 +83,7 @@ fn lengths_of(id: &str) -> Lengths {
|
||||
"threeD.revolve" => always(&["offset"]),
|
||||
"threeD.extrude" => always(&["depth", "bevelWidth", "bevelHeight"]),
|
||||
"threeD.classicExtrude" => always(&["depth", "bevelHeight"]),
|
||||
"threeD.classicRevolve" => always(&["offset"]),
|
||||
"threeD.inflate" => always(&["depth"]),
|
||||
"path.outlineStroke" => always(&["width"]),
|
||||
"stylize.roundCorners" | "stylize.feather" | "blur.gaussian" | "blur.smart" | "sharpen.unsharpMask" => always(&["radius"]),
|
||||
@@ -207,6 +210,13 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
|
||||
"{depth: pt (0..2000, 50), cap: bool (true: solid; false: hollow), bevel: none|classic|rounded|cove|step|ogee (none), bevelHeight: pt (1..100, 4; the bevel is as wide as it is tall), bevelInside: bool (false: added around the shape; true: carved into it), 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, hidden ones included, 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), drawHiddenFaces: bool (false; also draws the faces turned away, behind the others)}. The front face is the art, extruded backwards about the center of its bounds; a solid is built for the top fill and for each stroke, each in its own paint. The defaults are VectorCraft's own.",
|
||||
json!({"depth":50.0,"cap":true,"bevel":"none","bevelHeight":4.0,"bevelInside":false,"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","drawHiddenFaces":false}),
|
||||
),
|
||||
g(
|
||||
CLASSIC_REVOLVE,
|
||||
"Revolve (Classic)…",
|
||||
&["Effect", "3D and Materials", "3D (Classic)"],
|
||||
"{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(
|
||||
"convertToShape.rectangle",
|
||||
"Rectangle…",
|
||||
|
||||
@@ -83,7 +83,9 @@ pub use reshape::{
|
||||
SYMBOL_ART_MAX_NODES, expand_outlined, gives_image_shape, has_gradient_copies, has_item_geometry, has_painted_effects, has_transform_art,
|
||||
has_transform_copies, has_transform_options, image_paint, needs_outline, outline_art, outline_text, reshape,
|
||||
};
|
||||
pub use revolve::{REVOLVE, has_revolve, revolve_art, revolve_art_in, revolve_options, validate_revolve, validate_revolve_in};
|
||||
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,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Revolve as live, shaded vector art, shared by the canvas and every exporter.
|
||||
//! Revolve and Revolve (Classic) as live, shaded vector art, shared by the canvas and every
|
||||
//! exporter.
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::Value;
|
||||
@@ -13,6 +14,14 @@ use crate::{GeomContext, apply_geometry_with, is_geometry, merged_params};
|
||||
|
||||
pub const REVOLVE: &str = "threeD.revolve";
|
||||
|
||||
mod classic;
|
||||
pub use classic::CLASSIC_REVOLVE;
|
||||
|
||||
/// Is `id` Revolve or Revolve (Classic), the effects this module draws?
|
||||
pub fn is_revolve(id: &str) -> bool {
|
||||
id == REVOLVE || id == CLASSIC_REVOLVE
|
||||
}
|
||||
|
||||
/// Most bytes the copies of the object's paint in the faces may take together. Each face and the
|
||||
/// seam stroke around it gets a copy of the paint, so a gradient of many stops times tens of
|
||||
/// thousands of faces took gigabytes; past this the source path is drawn.
|
||||
@@ -55,7 +64,7 @@ pub(crate) fn paint_copies_add(used: &mut usize, copies: usize, paint: &Paint) -
|
||||
}
|
||||
|
||||
fn visible(fx: &[Effect]) -> bool {
|
||||
fx.iter().any(|e| e.visible && e.id == REVOLVE)
|
||||
fx.iter().any(|e| e.visible && is_revolve(&e.id))
|
||||
}
|
||||
|
||||
pub fn has_revolve(n: &Node) -> bool {
|
||||
@@ -88,6 +97,17 @@ pub fn revolve_options(p: &Value) -> Revolve {
|
||||
}
|
||||
}
|
||||
|
||||
/// The options Revolve (`id` [`REVOLVE`]) or Revolve (Classic) ([`CLASSIC_REVOLVE`]) with
|
||||
/// parameters `p` sweeps its surface with.
|
||||
fn options_of(id: &str, p: &Value) -> Revolve {
|
||||
if id == CLASSIC_REVOLVE { classic::options(p) } else { revolve_options(p) }
|
||||
}
|
||||
|
||||
/// The options effect `fx`, a Revolve or a Revolve (Classic), sweeps its surface with.
|
||||
fn fx_options(fx: &Effect) -> Revolve {
|
||||
options_of(&fx.id, &merged_params(&fx.id, &fx.params))
|
||||
}
|
||||
|
||||
/// Validate before a command mutates the document. Loading external files is still bounded
|
||||
/// by the evaluator; unsupported source kinds remain unchanged when rendered.
|
||||
pub fn validate_revolve(n: &Node, p: &Value) -> Result<(), String> {
|
||||
@@ -97,11 +117,16 @@ pub fn validate_revolve(n: &Node, p: &Value) -> Result<(), String> {
|
||||
/// [`validate_revolve`] with the symbols of `doc`: a symbol instance, a blend and a clipping group
|
||||
/// revolve the art they show ([`resolve_for`]), cut for Revolve whatever 3D effect `n` carries now.
|
||||
pub fn validate_revolve_in(doc: Option<&Document>, n: &Node, p: &Value) -> Result<(), String> {
|
||||
validate_revolve_params(p)?;
|
||||
validate_in(doc, n, REVOLVE, p)
|
||||
}
|
||||
|
||||
/// [`validate_revolve_in`] for Revolve or Revolve (Classic), `id`.
|
||||
pub(crate) fn validate_in(doc: Option<&Document>, n: &Node, id: &str, p: &Value) -> Result<(), String> {
|
||||
validate_params_of(id, p)?;
|
||||
let n = &*resolve_for(doc, n, "Revolve", false)?;
|
||||
let effects = &n.appearance.effects;
|
||||
// The profile is measured before the object's one Revolve, hidden or shown.
|
||||
let before = effects.iter().position(|e| e.id == REVOLVE).and_then(|i| effects.get(..i)).unwrap_or(effects);
|
||||
let before = effects.iter().position(|e| is_revolve(&e.id)).and_then(|i| effects.get(..i)).unwrap_or(effects);
|
||||
if is_container(n) {
|
||||
const EMPTY: &str = "Revolve requires a group holding a shape with at least two anchors";
|
||||
let mut shapes = members(n, "Revolve")?;
|
||||
@@ -110,7 +135,7 @@ pub fn validate_revolve_in(doc: Option<&Document>, n: &Node, p: &Value) -> Resul
|
||||
}
|
||||
reshape_members(n, before, &mut shapes);
|
||||
let b = union_bounds(&shapes).ok_or(EMPTY)?;
|
||||
let options = shared_options(&shapes, revolve_options(p))?;
|
||||
let options = shared_options(&shapes, options_of(id, p))?;
|
||||
let mut copies = 0;
|
||||
shapes.iter().try_for_each(|s| check_surface(&s.path, b, options, surface_item(&s.items), &mut copies))?;
|
||||
let casting = shapes.iter().filter(|s| surface_item(&s.items).is_some_and(|i| paint_of(i).1 * s.opacity > 0.0));
|
||||
@@ -123,8 +148,8 @@ pub fn validate_revolve_in(doc: Option<&Document>, n: &Node, p: &Value) -> Resul
|
||||
let path = geometry(&path, before, &GeomContext::of(n));
|
||||
let b = path.bounds().ok_or("Revolve requires a shape with at least two anchors")?;
|
||||
let paints = solid_paints(n);
|
||||
check_surface(&path, b, revolve_options(p), surface_item(&paints), &mut 0)?;
|
||||
check_shadow(p, [&path], b, revolve_options(p))
|
||||
check_surface(&path, b, options_of(id, p), surface_item(&paints), &mut 0)?;
|
||||
check_shadow(p, [&path], b, options_of(id, p))
|
||||
}
|
||||
|
||||
/// With the shadow of Revolve parameters `p` on, an error when the surfaces of `paths` it is cast
|
||||
@@ -138,14 +163,21 @@ fn check_shadow<'a>(p: &Value, paths: impl IntoIterator<Item = &'a PathData>, b:
|
||||
}
|
||||
|
||||
/// [`validate_revolve`] for a Revolve with parameters `p` at index `index` of the effects of
|
||||
/// appearance item `target` of `n`. The surfaces of every fill and stroke that carries a Revolve,
|
||||
/// `target`'s included, share one budget of paint copies, as in [`art`]. A visible object-level
|
||||
/// Revolve draws in place of those surfaces.
|
||||
/// appearance item `target` of `n` ([`validate_item_of`]).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn validate_item_revolve(n: &Node, p: &Value, target: usize, index: usize) -> Result<(), String> {
|
||||
validate_item_of(n, REVOLVE, p, target, index)
|
||||
}
|
||||
|
||||
/// [`validate_revolve`] for a Revolve or Revolve (Classic), `id`, with parameters `p` at index
|
||||
/// `index` of the effects of appearance item `target` of `n`. The surfaces of every fill and stroke
|
||||
/// that carries one, `target`'s included, share one budget of paint copies, as in [`art`]. A visible
|
||||
/// object-level Revolve draws in place of those surfaces.
|
||||
pub(crate) fn validate_item_of(n: &Node, id: &str, p: &Value, target: usize, index: usize) -> Result<(), String> {
|
||||
if is_container(n) || visible(&n.appearance.effects) || target >= n.appearance.items.len() {
|
||||
return validate_revolve(n, p);
|
||||
return validate_in(None, n, id, p);
|
||||
}
|
||||
validate_revolve_params(p)?;
|
||||
validate_params_of(id, p)?;
|
||||
const EMPTY: &str = "Revolve requires a shape with at least two anchors";
|
||||
let (path, _) = object_profile(n, "Revolve")?;
|
||||
if !has_two_anchors(&path) {
|
||||
@@ -156,13 +188,13 @@ pub(crate) fn validate_item_revolve(n: &Node, p: &Value, target: usize, index: u
|
||||
let mut copies = 0;
|
||||
for (i, item) in n.appearance.items.iter().enumerate() {
|
||||
let effects = item.effects();
|
||||
let at = if i == target { Some(index) } else { effects.iter().position(|e| e.visible && e.id == REVOLVE) };
|
||||
let params = match at.and_then(|at| effects.get(at)) {
|
||||
_ if i == target => p.clone(),
|
||||
Some(fx) if crate::paints(item) => merged_params(REVOLVE, &fx.params),
|
||||
let at = if i == target { Some(index) } else { effects.iter().position(|e| e.visible && is_revolve(&e.id)) };
|
||||
let (kind, params) = match at.and_then(|at| effects.get(at)) {
|
||||
_ if i == target => (id, p.clone()),
|
||||
Some(fx) if crate::paints(item) => (fx.id.as_str(), merged_params(&fx.id, &fx.params)),
|
||||
_ => continue,
|
||||
};
|
||||
let options = revolve_options(¶ms);
|
||||
let options = options_of(kind, ¶ms);
|
||||
let path = geometry(&path, at.and_then(|at| effects.get(..at)).unwrap_or(effects), &ctx.item(item));
|
||||
let b = path.bounds().ok_or(EMPTY)?;
|
||||
check_surface(&path, b, options, crate::paints(item).then_some(item), &mut copies)?;
|
||||
@@ -217,6 +249,16 @@ pub(crate) fn validate_revolve_params(p: &Value) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// [`validate_revolve_params`] for Revolve or Revolve (Classic), `id`; Revolve (Classic) also takes
|
||||
/// only its own parameters, each in its range.
|
||||
pub(crate) fn validate_params_of(id: &str, p: &Value) -> Result<(), String> {
|
||||
validate_revolve_params(p)?;
|
||||
if id == CLASSIC_REVOLVE {
|
||||
classic::validate(p)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn geometry(path: &PathData, fx: &[Effect], ctx: &GeomContext) -> PathData {
|
||||
apply_geometry_with(fx, path, path.bounds().unwrap_or(Rect::ZERO), ctx)
|
||||
}
|
||||
@@ -226,7 +268,7 @@ fn geometry(path: &PathData, fx: &[Effect], ctx: &GeomContext) -> PathData {
|
||||
/// the shadow off, when `item` is fully transparent or when it casts nothing, and an error when
|
||||
/// the surface it is cast from is past the limits.
|
||||
fn shadow_of(id: NodeId, fx: &Effect, path: &PathData, item: &AppearanceItem, b: Rect, options: Revolve) -> Result<Option<Node>, String> {
|
||||
let Some(settings) = crate::solids::shadow::options(&merged_params(REVOLVE, &fx.params)) else { return Ok(None) };
|
||||
let Some(settings) = crate::solids::shadow::options(&merged_params(&fx.id, &fx.params)) else { return Ok(None) };
|
||||
let (_, opacity, blend) = paint_of(item);
|
||||
if opacity <= 0.0 {
|
||||
return Ok(None);
|
||||
@@ -242,7 +284,7 @@ fn shadow_of(id: NodeId, fx: &Effect, path: &PathData, item: &AppearanceItem, b:
|
||||
/// ([`crate::solids::shadow::scoped_art`]). An error when the surfaces it is cast from are past the
|
||||
/// limits.
|
||||
fn group_shadow(id: NodeId, fx: &Effect, shapes: &[Profile], b: Rect, options: Revolve) -> Result<Vec<Arc<Node>>, String> {
|
||||
let Some(settings) = crate::solids::shadow::options(&merged_params(REVOLVE, &fx.params)) else { return Ok(vec![]) };
|
||||
let Some(settings) = crate::solids::shadow::options(&merged_params(&fx.id, &fx.params)) else { return Ok(vec![]) };
|
||||
let mut caster = vectorcraft_three_d::ShadowCaster::new();
|
||||
let mut cast = vec![];
|
||||
for s in shapes {
|
||||
@@ -259,7 +301,8 @@ fn group_shadow(id: NodeId, fx: &Effect, shapes: &[Profile], b: Rect, options: R
|
||||
}
|
||||
|
||||
/// One object's surface of revolution about the axis of `b`, and each face's depth. `copies` counts
|
||||
/// the bytes of the paint copies of every surface of the object.
|
||||
/// the bytes of the paint copies of every surface of the object. Revolve (Classic) draws every face
|
||||
/// with its classic surface, as Extrude & Bevel (Classic) does.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn surface(
|
||||
n: &Node,
|
||||
@@ -275,6 +318,16 @@ fn surface(
|
||||
) -> Option<(Vec<f64>, Node)> {
|
||||
// A loaded file or a profile edited past the limits keeps drawing its source path instead
|
||||
// of crashing the frame. Commands validate the same evaluator and report its errors.
|
||||
if fx.id == CLASSIC_REVOLVE {
|
||||
let facets = vectorcraft_three_d::revolve_facets(path, b, options).ok()?;
|
||||
*copies = copies.saturating_add(paint_copies(facets.len(), &paint));
|
||||
if *copies > MAX_PAINT_COPIES {
|
||||
return None;
|
||||
}
|
||||
let depths = facets.iter().map(|f| f.depth).collect();
|
||||
let p = merged_params(&fx.id, &fx.params);
|
||||
return Some((depths, crate::solids::classic::surface(n, facets, &paint, opacity, blend, &p)));
|
||||
}
|
||||
let colored = options.light_color != [1.0; 3] || options.lights.is_some();
|
||||
let faces = vectorcraft_three_d::revolve(path, b, options).ok()?;
|
||||
*copies = copies.saturating_add(paint_copies(faces.len(), &paint));
|
||||
@@ -371,8 +424,7 @@ fn surface(
|
||||
fn object_surface(n: &Node, path: &PathData, fx: &Effect, item: &AppearanceItem, expand: bool, copies: &mut usize) -> Option<Node> {
|
||||
let b = path.bounds()?;
|
||||
let (paint, opacity, blend) = paint_of(item);
|
||||
let options = revolve_options(&merged_params(REVOLVE, &fx.params));
|
||||
surface(n, path, b, options, fx, paint, opacity, blend, expand, copies).map(|(_, art)| art)
|
||||
surface(n, path, b, fx_options(fx), fx, paint, opacity, blend, expand, copies).map(|(_, art)| art)
|
||||
}
|
||||
|
||||
fn shading_paint(g: vectorcraft_three_d::ShadeGradient, color: impl Fn(f64) -> Color, opacity: impl Fn(f64) -> f32) -> Paint {
|
||||
@@ -429,10 +481,10 @@ pub(crate) fn revolve_meshes(n: &Node, total: &mut usize, limit: usize) -> Resul
|
||||
let mesh = |path: &PathData, fx: &Effect, item: &AppearanceItem| -> Result<crate::SolidMesh, String> {
|
||||
let b = path.bounds().ok_or("Revolve requires a shape with at least two anchors")?;
|
||||
let (paint, opacity, _) = paint_of(item);
|
||||
let mesh = vectorcraft_three_d::revolve_mesh(path, rule, b, revolve_options(&merged_params(REVOLVE, &fx.params)))?;
|
||||
let mesh = vectorcraft_three_d::revolve_mesh(path, rule, b, fx_options(fx))?;
|
||||
Ok(crate::SolidMesh { mesh, color: crate::solids::mesh_color(&paint, opacity) })
|
||||
};
|
||||
if let Some(index) = n.appearance.effects.iter().position(|e| e.visible && e.id == REVOLVE) {
|
||||
if let Some(index) = n.appearance.effects.iter().position(|e| e.visible && is_revolve(&e.id)) {
|
||||
let before = n.appearance.effects.get(..index).unwrap_or_default();
|
||||
let Some(fx) = n.appearance.effects.get(index) else { return Ok(vec![]) };
|
||||
let path = geometry(&path, before, &ctx);
|
||||
@@ -445,7 +497,7 @@ pub(crate) fn revolve_meshes(n: &Node, total: &mut usize, limit: usize) -> Resul
|
||||
let path = geometry(&path, &n.appearance.effects, &ctx);
|
||||
let mut out = vec![];
|
||||
for item in n.appearance.items.iter().filter(|i| crate::paints(i)) {
|
||||
if let Some(index) = item.effects().iter().position(|e| e.visible && e.id == REVOLVE)
|
||||
if let Some(index) = item.effects().iter().position(|e| e.visible && is_revolve(&e.id))
|
||||
&& let Some(fx) = item.effects().get(index)
|
||||
{
|
||||
let p = geometry(&path, item.effects().get(..index).unwrap_or_default(), &ctx.item(item));
|
||||
@@ -461,13 +513,13 @@ fn group_meshes(n: &Node, total: &mut usize, limit: usize) -> Result<Vec<crate::
|
||||
.appearance
|
||||
.effects
|
||||
.iter()
|
||||
.position(|e| e.visible && e.id == REVOLVE)
|
||||
.position(|e| e.visible && is_revolve(&e.id))
|
||||
.ok_or("a 3D effect on a group's fill or stroke draws nothing to export")?;
|
||||
let Some(fx) = n.appearance.effects.get(index) else { return Ok(vec![]) };
|
||||
let mut shapes = members(n, "Revolve")?;
|
||||
reshape_members(n, n.appearance.effects.get(..index).unwrap_or_default(), &mut shapes);
|
||||
let b = union_bounds(&shapes).ok_or("Revolve requires a group holding a shape with at least two anchors")?;
|
||||
let options = shared_options(&shapes, revolve_options(&merged_params(REVOLVE, &fx.params)))?;
|
||||
let options = shared_options(&shapes, fx_options(fx))?;
|
||||
let mut out = vec![];
|
||||
for s in &shapes {
|
||||
let Some(item) = surface_item(&s.items) else { continue };
|
||||
@@ -491,7 +543,7 @@ fn art(doc: Option<&Document>, n: &Node, expand: bool) -> Option<Node> {
|
||||
let mut children = vec![];
|
||||
let mut copies = 0;
|
||||
let mut remaining = n.appearance.effects.clone();
|
||||
if let Some(index) = n.appearance.effects.iter().position(|e| e.visible && e.id == REVOLVE) {
|
||||
if let Some(index) = n.appearance.effects.iter().position(|e| e.visible && is_revolve(&e.id)) {
|
||||
let fx = n.appearance.effects.get(index)?;
|
||||
let path = geometry(&path, n.appearance.effects.get(..index)?, &ctx);
|
||||
// An object's surface uses its top visible fill, falling back to its stroke.
|
||||
@@ -500,7 +552,7 @@ fn art(doc: Option<&Document>, n: &Node, expand: bool) -> Option<Node> {
|
||||
let expand = expand && !n.appearance.effects.iter().skip(index + 1).any(|e| e.visible && is_geometry(&e.id));
|
||||
let surface = object_surface(n, &path, fx, item, expand, &mut copies)?;
|
||||
if let Some(b) = path.bounds()
|
||||
&& let Some(shadow) = shadow_of(n.id, fx, &path, item, b, revolve_options(&merged_params(REVOLVE, &fx.params))).ok()?
|
||||
&& let Some(shadow) = shadow_of(n.id, fx, &path, item, b, fx_options(fx)).ok()?
|
||||
{
|
||||
children.push(Arc::new(shadow));
|
||||
}
|
||||
@@ -511,7 +563,7 @@ fn art(doc: Option<&Document>, n: &Node, expand: bool) -> Option<Node> {
|
||||
.effects
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, e)| e.id != REVOLVE && (!is_geometry(&e.id) || *i > index))
|
||||
.filter(|(i, e)| !is_revolve(&e.id) && (!is_geometry(&e.id) || *i > index))
|
||||
.map(|(_, e)| e.clone())
|
||||
.collect();
|
||||
} else {
|
||||
@@ -520,14 +572,14 @@ fn art(doc: Option<&Document>, n: &Node, expand: bool) -> Option<Node> {
|
||||
// Every item's shadow goes behind every item's art.
|
||||
let mut shadows = vec![];
|
||||
for item in n.appearance.items.iter().filter(|i| crate::paints(i)) {
|
||||
if let Some(index) = item.effects().iter().position(|e| e.visible && e.id == REVOLVE) {
|
||||
if let Some(index) = item.effects().iter().position(|e| e.visible && is_revolve(&e.id)) {
|
||||
let fx = item.effects().get(index)?;
|
||||
let p = geometry(&path, item.effects().get(..index)?, &ctx.item(item));
|
||||
let expand = expand && !item.effects().iter().skip(index + 1).any(|e| e.visible && is_geometry(&e.id));
|
||||
let mut art = object_surface(n, &p, fx, item, expand, &mut copies)?;
|
||||
art.appearance.effects = item.effects().iter().skip(index + 1).filter(|e| e.id != REVOLVE).cloned().collect();
|
||||
art.appearance.effects = item.effects().iter().skip(index + 1).filter(|e| !is_revolve(&e.id)).cloned().collect();
|
||||
if let Some(b) = p.bounds()
|
||||
&& let Some(mut shadow) = shadow_of(n.id, fx, &p, item, b, revolve_options(&merged_params(REVOLVE, &fx.params))).ok()?
|
||||
&& let Some(mut shadow) = shadow_of(n.id, fx, &p, item, b, fx_options(fx)).ok()?
|
||||
{
|
||||
shadow.appearance.effects = art.appearance.effects.clone();
|
||||
shadows.push(Arc::new(shadow));
|
||||
@@ -666,12 +718,12 @@ fn overlaps(a: Rect, b: Rect) -> bool {
|
||||
/// each in its own paint. Every member's faces are drawn in one back-to-front order. `None` when
|
||||
/// the effect is only on the group's own fills or strokes or when its members can't be revolved.
|
||||
fn group_art(n: &Node, expand: bool) -> Option<Node> {
|
||||
let index = n.appearance.effects.iter().position(|e| e.visible && e.id == REVOLVE)?;
|
||||
let index = n.appearance.effects.iter().position(|e| e.visible && is_revolve(&e.id))?;
|
||||
let fx = n.appearance.effects.get(index)?;
|
||||
let mut shapes = members(n, "").ok()?;
|
||||
reshape_members(n, n.appearance.effects.get(..index)?, &mut shapes);
|
||||
let b = union_bounds(&shapes)?;
|
||||
let options = shared_options(&shapes, revolve_options(&merged_params(REVOLVE, &fx.params))).ok()?;
|
||||
let options = shared_options(&shapes, fx_options(fx)).ok()?;
|
||||
let expand = expand && !n.appearance.effects.iter().skip(index + 1).any(|e| e.visible && is_geometry(&e.id));
|
||||
let mut copies = 0;
|
||||
let mut surfaces = vec![];
|
||||
@@ -689,7 +741,10 @@ fn group_art(n: &Node, expand: bool) -> Option<Node> {
|
||||
if surfaces.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let plain = surfaces.iter().all(|(depths, s, _)| matches!(&s.kind, NodeKind::Group { children, clip: false } if children.len() == depths.len()));
|
||||
// A classic surface whose gradient has transparent stops carries an opacity mask, and stays whole.
|
||||
let plain = surfaces.iter().all(|(depths, s, _)| {
|
||||
s.extras().mask.is_none() && matches!(&s.kind, NodeKind::Group { children, clip: false } if children.len() == depths.len())
|
||||
});
|
||||
let children = if !plain {
|
||||
scoped(n.id, surfaces.into_iter().map(|(_, s, scopes)| (scopes, Arc::new(s))).collect(), 0)
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
//! Revolve (Classic): the surface Revolve sweeps from a path, drawn with the classic surfaces of
|
||||
//! Extrude & Bevel (Classic) (Wireframe, No Shading, Diffuse and Plastic), their highlight, blend
|
||||
//! steps and shading color.
|
||||
use serde_json::Value;
|
||||
use vectorcraft_three_d::Revolve;
|
||||
|
||||
pub const CLASSIC_REVOLVE: &str = "threeD.classicRevolve";
|
||||
|
||||
/// The numeric parameters: (key, lowest, highest, whole numbers only).
|
||||
const RANGES: [(&str, f64, f64, bool); 13] = [
|
||||
("angle", 0.0, 360.0, false),
|
||||
("offset", 0.0, 1000.0, false),
|
||||
("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),
|
||||
("highlightIntensity", 0.0, 100.0, false),
|
||||
("highlightSize", 0.0, 100.0, false),
|
||||
("blendSteps", 1.0, 256.0, true),
|
||||
];
|
||||
|
||||
const FLAGS: [&str; 2] = ["cap", "lightBehind"];
|
||||
|
||||
/// The surface Revolve (Classic)'s parameters `p` sweep: Revolve's angle, cap, offset, edge,
|
||||
/// rotation and perspective, built unshaded, since the classic surfaces light its faces.
|
||||
pub(crate) fn options(p: &Value) -> Revolve {
|
||||
Revolve { shade: false, smooth_shading: false, lights: None, light_color: [1.0; 3], ..super::revolve_options(p) }
|
||||
}
|
||||
|
||||
/// Check Revolve (Classic)'s parameters `p` (its defaults filled in) beyond the checks it shares
|
||||
/// with Revolve: every key is one of its parameters, numbers are in range, flags are true or false,
|
||||
/// and the surface and shading color are named.
|
||||
pub(crate) fn validate(p: &Value) -> Result<(), String> {
|
||||
crate::solids::classic::validate_classic(CLASSIC_REVOLVE, p, &RANGES, &FLAGS)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
use vectorcraft_doc::color::{Color, Paint};
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, Node, NodeId};
|
||||
use vectorcraft_geom::{BezPath, PathData};
|
||||
|
||||
use super::*;
|
||||
|
||||
const BLUE: Color = Color::Rgb { r: 0.2, g: 0.4, b: 0.8 };
|
||||
|
||||
/// Half a vase's profile, open, filled blue.
|
||||
fn profile() -> Node {
|
||||
let mut b = BezPath::new();
|
||||
b.move_to((100.0, 40.0));
|
||||
b.line_to((130.0, 60.0));
|
||||
b.line_to((130.0, 140.0));
|
||||
b.line_to((100.0, 160.0));
|
||||
Node::path(NodeId(1), PathData::from_bezpath(&b), Appearance::basic(Paint::solid(BLUE), Paint::None, 0.0))
|
||||
}
|
||||
|
||||
fn revolved(params: Value) -> Node {
|
||||
let mut n = profile();
|
||||
n.appearance.effects.push(crate::new_effect(CLASSIC_REVOLVE, ¶ms).unwrap());
|
||||
crate::revolve_art(&n).unwrap()
|
||||
}
|
||||
|
||||
/// The faces of the surface of `art`, back to front.
|
||||
fn faces(art: &Node) -> Vec<&Node> {
|
||||
art.children().unwrap().iter().flat_map(|s| s.children().unwrap().iter().map(|f| &**f)).collect()
|
||||
}
|
||||
|
||||
fn fill(face: &Node) -> Option<Color> {
|
||||
face.appearance.items.iter().find_map(|i| match i {
|
||||
AppearanceItem::Fill(f) => f.paint.color(),
|
||||
AppearanceItem::Stroke(_) => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn facets(params: &Value) -> usize {
|
||||
let n = profile();
|
||||
let path = n.path_data().unwrap();
|
||||
let p = crate::merged_params(CLASSIC_REVOLVE, params);
|
||||
vectorcraft_three_d::revolve_facets(path, path.bounds().unwrap(), options(&p)).unwrap().len()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plastic_shades_every_face_of_the_swept_surface_in_the_fill() {
|
||||
let art = revolved(json!({}));
|
||||
let drawn = faces(&art);
|
||||
assert_eq!(drawn.len(), facets(&json!({})), "one face for each facet of the surface");
|
||||
let colors: Vec<[f32; 3]> = drawn.iter().map(|f| fill(f).unwrap().to_rgb()).collect();
|
||||
// Lit from one side, the faces take more than one tone, and none is another hue: a face is
|
||||
// the blue darkened toward black, lightened toward white, or both.
|
||||
let distinct = colors.iter().filter(|c| colors.iter().any(|d| (c[0] - d[0]).abs() > 0.02)).count();
|
||||
assert!(distinct > 0, "{colors:?}");
|
||||
// Diffuse has no highlight, so every face is the blue darkened toward black.
|
||||
let art = revolved(json!({"surface": "diffuse"}));
|
||||
for face in faces(&art) {
|
||||
let [r, g, b] = fill(face).unwrap().to_rgb();
|
||||
assert!(b <= 0.8 + 1e-6 && (g * 2.0 - b).abs() < 1e-4 && (r * 4.0 - b).abs() < 1e-4, "{r} {g} {b}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wireframe_strokes_the_edges_of_every_face_and_no_shading_keeps_the_paint() {
|
||||
let art = revolved(json!({"surface": "wireframe"}));
|
||||
let faces_drawn = faces(&art);
|
||||
assert_eq!(faces_drawn.len(), facets(&json!({"surface": "wireframe"})));
|
||||
for face in faces_drawn {
|
||||
match face.appearance.items.as_slice() {
|
||||
[AppearanceItem::Stroke(s)] => assert_eq!((s.paint.color(), s.width), (Some(BLUE), 0.5)),
|
||||
items => panic!("a wireframe face is one stroke: {items:?}"),
|
||||
}
|
||||
}
|
||||
for face in faces(&revolved(json!({"surface": "none"}))) {
|
||||
assert_eq!(fill(face), Some(BLUE));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn angle_offset_edge_and_cap_shape_the_surface() {
|
||||
let full = revolved(json!({"rotationX": 0, "rotationY": 0, "rotationZ": 0})).visual_bounds().unwrap();
|
||||
// Seen from the front, the full turn is twice the profile's width across, about its left edge.
|
||||
assert!((full.width() - 60.0).abs() < 2.0, "{full:?}");
|
||||
let ring = revolved(json!({"rotationX": 0, "rotationY": 0, "rotationZ": 0, "offset": 20})).visual_bounds().unwrap();
|
||||
assert!((ring.width() - 100.0).abs() < 2.0, "{ring:?}");
|
||||
// About its right edge the axis is at x 130, so the surface reaches x 160.
|
||||
let right = revolved(json!({"rotationX": 0, "rotationY": 0, "rotationZ": 0, "edge": "right"})).visual_bounds().unwrap();
|
||||
assert!((right.x1 - 160.0).abs() < 2.0, "{right:?}");
|
||||
assert!(facets(&json!({"angle": 90})) < facets(&json!({})));
|
||||
// The profile's ends lie on the axis, so they close nothing until an offset moves them off it.
|
||||
assert!(facets(&json!({"cap": false, "offset": 10})) < facets(&json!({"offset": 10})), "hollow leaves out the end caps");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_group_revolves_its_members_about_one_shared_axis() {
|
||||
let mut b = BezPath::new();
|
||||
b.move_to((150.0, 60.0));
|
||||
b.line_to((170.0, 60.0));
|
||||
b.line_to((170.0, 140.0));
|
||||
let outer = Node::path(NodeId(2), PathData::from_bezpath(&b), Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0));
|
||||
let mut g = Node::group(NodeId(3), vec![std::sync::Arc::new(profile()), std::sync::Arc::new(outer)]);
|
||||
g.appearance.effects.push(crate::new_effect(CLASSIC_REVOLVE, &json!({"rotationX": 0, "rotationY": 0, "rotationZ": 0})).unwrap());
|
||||
let art = crate::revolve_art(&g).unwrap();
|
||||
// The axis is at the left edge of both members together, x 100, so the outer one reaches 70
|
||||
// out on either side of it.
|
||||
let bounds = art.visual_bounds().unwrap();
|
||||
assert!((bounds.x0 - 30.0).abs() < 2.0 && (bounds.x1 - 170.0).abs() < 2.0, "{bounds:?}");
|
||||
let mut colors = vec![];
|
||||
art.walk(&mut |n| colors.extend(fill(n).map(|c| c.to_rgb())));
|
||||
assert!(colors.iter().any(|c| c[2] > c[0]) && colors.iter().any(|c| c[0] == c[2]), "both members' paints are drawn");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_parameters_are_refused() {
|
||||
let check = |p: Value| crate::validate_three_d(&profile(), CLASSIC_REVOLVE, &crate::merged_params(CLASSIC_REVOLVE, &p));
|
||||
assert_eq!(check(json!({})), Ok(()));
|
||||
for (p, error) in [
|
||||
(json!({"segments": 32}), "unknown parameter `segments`"),
|
||||
(json!({"angle": 400}), "`angle` must be between 0 and 360"),
|
||||
(json!({"offset": 1001}), "`offset` must be between 0 and 1000"),
|
||||
(json!({"rotationY": -181}), "`rotationY` must be between -180 and 180"),
|
||||
(json!({"blendSteps": 2.5}), "`blendSteps` must be a whole number"),
|
||||
(json!({"lightBehind": 1}), "`lightBehind` must be true or false"),
|
||||
(json!({"surface": "glossy"}), "`surface` must be one of"),
|
||||
(json!({"shadingColor": "white"}), "`shadingColor` must be one of"),
|
||||
(json!({"shadingCustom": "red"}), "`shadingCustom` must be a #rrggbb color"),
|
||||
(json!({"edge": "middle"}), "Revolve edge must be left or right"),
|
||||
(json!({"cap": "yes"}), "Revolve cap must be a boolean"),
|
||||
] {
|
||||
let e = check(p.clone()).unwrap_err();
|
||||
assert!(e.contains(error), "{p}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ use vectorcraft_doc::color::Color;
|
||||
use crate::util::{flag, num, text};
|
||||
use crate::{GeomContext, apply_geometry_with, is_geometry};
|
||||
|
||||
mod classic;
|
||||
pub(crate) mod classic;
|
||||
pub use classic::{CLASSIC_EXTRUDE, SHADING_COLORS, classic_extrude_options, classic_shading};
|
||||
|
||||
mod source;
|
||||
@@ -28,9 +28,9 @@ 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];
|
||||
|
||||
/// Is `id` one of the 3D and Materials effects (Revolve included)?
|
||||
/// Is `id` one of the 3D and Materials effects (Revolve and Revolve (Classic) included)?
|
||||
pub fn is_three_d(id: &str) -> bool {
|
||||
id == crate::REVOLVE || SOLIDS.contains(&id)
|
||||
crate::revolve::is_revolve(id) || SOLIDS.contains(&id)
|
||||
}
|
||||
|
||||
/// The 3D and Materials panel's object types and the effect that draws each: (type, effect id).
|
||||
@@ -417,7 +417,7 @@ pub fn validate_three_d(n: &Node, id: &str, p: &Value) -> Result<(), String> {
|
||||
|
||||
/// [`validate_three_d`] with the symbols of `doc` ([`validate_solid_in`]).
|
||||
pub fn validate_three_d_in(doc: Option<&Document>, n: &Node, id: &str, p: &Value) -> Result<(), String> {
|
||||
if id == crate::REVOLVE { crate::revolve::validate_revolve_in(doc, n, p) } else { validate_solid_in(doc, n, id, p) }
|
||||
if crate::revolve::is_revolve(id) { crate::revolve::validate_in(doc, n, id, p) } else { validate_solid_in(doc, n, id, p) }
|
||||
}
|
||||
|
||||
/// [`validate_three_d`] for an effect at `(item, index)`, index `index` of the effects of
|
||||
@@ -436,7 +436,7 @@ pub fn validate_three_d_at_in(doc: Option<&Document>, n: &Node, id: &str, p: &Va
|
||||
Some(_) if is_container(n) || source::resolves(n) => {
|
||||
Err("A 3D effect on a group or layer goes on the group itself, not on its fill or stroke".into())
|
||||
}
|
||||
Some((item, index)) if id == crate::REVOLVE => crate::revolve::validate_item_revolve(n, p, item, index),
|
||||
Some((item, index)) if crate::revolve::is_revolve(id) => crate::revolve::validate_item_of(n, id, p, item, index),
|
||||
Some((item, _)) => validate_solid_for(doc, n, id, p, Some(item)),
|
||||
None => validate_three_d_in(doc, n, id, p),
|
||||
}
|
||||
@@ -446,7 +446,7 @@ pub fn validate_three_d_at_in(doc: Option<&Document>, n: &Node, id: &str, p: &Va
|
||||
/// build the art, for an edit that hides the effect: hidden, it draws nothing, so its object does
|
||||
/// not need to fit.
|
||||
pub fn validate_three_d_params(id: &str, p: &Value) -> Result<(), String> {
|
||||
if id == crate::REVOLVE { crate::revolve::validate_revolve_params(p) } else { validate_solid_params(id, p) }
|
||||
if crate::revolve::is_revolve(id) { crate::revolve::validate_params_of(id, p) } else { validate_solid_params(id, p) }
|
||||
}
|
||||
|
||||
/// One surface: each facet a path shaded from `paint`, as a group with the paint's opacity and
|
||||
|
||||
@@ -104,8 +104,16 @@ const FLAGS: [&str; 4] = ["cap", "bevelInside", "lightBehind", "drawHiddenFaces"
|
||||
/// its parameters, numbers are in range, flags are true or false, and `surface`, `shadingColor` and
|
||||
/// `shadingCustom` name a surface, a shading color and a #rrggbb color.
|
||||
pub fn validate(p: &Value) -> Result<(), String> {
|
||||
validate_classic(CLASSIC_EXTRUDE, p, &RANGES, &FLAGS)
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// surface, a shading color and a #rrggbb color.
|
||||
pub(crate) fn validate_classic(id: &str, p: &Value, ranges: &[(&str, f64, f64, bool)], flags: &[&str]) -> Result<(), String> {
|
||||
let Some(map) = p.as_object() else { return Ok(()) };
|
||||
let defaults = crate::default_params(CLASSIC_EXTRUDE).unwrap_or(Value::Null);
|
||||
let defaults = crate::default_params(id).unwrap_or(Value::Null);
|
||||
let known: Vec<&str> = defaults.as_object().map(|m| m.keys().map(String::as_str).collect()).unwrap_or_default();
|
||||
for key in map.keys() {
|
||||
if !known.contains(&key.as_str()) {
|
||||
@@ -113,7 +121,7 @@ pub fn validate(p: &Value) -> Result<(), String> {
|
||||
return Err(format!("unknown parameter `{key}`; the command takes: {keys}"));
|
||||
}
|
||||
}
|
||||
for (key, min, max, whole) in RANGES {
|
||||
for &(key, min, max, whole) in ranges {
|
||||
let Some(v) = map.get(key) else { continue };
|
||||
let Some(x) = v.as_f64() else { return Err(format!("`{key}` must be a number")) };
|
||||
if whole && (x.fract() != 0.0 || !(min..=max).contains(&x)) {
|
||||
@@ -123,7 +131,7 @@ pub fn validate(p: &Value) -> Result<(), String> {
|
||||
return Err(format!("`{key}` must be between {min} and {max} (got {x})"));
|
||||
}
|
||||
}
|
||||
for key in FLAGS {
|
||||
for &key in flags {
|
||||
if map.get(key).is_some_and(|v| !v.is_boolean()) {
|
||||
return Err(format!("`{key}` must be true or false"));
|
||||
}
|
||||
@@ -161,7 +169,7 @@ fn shaded(c: Color, tone: f64, spec: f64, shade: Option<Color>) -> Color {
|
||||
/// for each facet: the edges of the faces on Wireframe, else each face filled in its paint shaded by
|
||||
/// its tone and highlight. A gradient with transparent stops is shaded opaque and then masked by
|
||||
/// its own transparency, so the shading follows the gradient's coverage.
|
||||
pub(super) fn surface(n: &Node, facets: Vec<Facet>, source: &Paint, opacity: f32, blend: BlendMode, p: &Value) -> Node {
|
||||
pub(crate) fn surface(n: &Node, facets: Vec<Facet>, source: &Paint, opacity: f32, blend: BlendMode, p: &Value) -> Node {
|
||||
let shading = classic_shading(p);
|
||||
let lights = lights(p);
|
||||
let shade = shading_color(p);
|
||||
|
||||
@@ -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), which has 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) or Revolve (Classic), which have no lighting presets; one undo step → {ids}",
|
||||
has_doc,
|
||||
lighting_preset
|
||||
),
|
||||
@@ -656,12 +656,10 @@ fn lighting_preset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if !effect.is_some_and(|e| effects::is_three_d(&e.id)) {
|
||||
return Err(bad(C, format!("object {} needs a 3D effect at index {index}", id.0)));
|
||||
}
|
||||
// Extrude & Bevel (Classic) has no lighting presets; its lights are set in its own options.
|
||||
if effect.is_some_and(|e| e.id == effects::CLASSIC_EXTRUDE) {
|
||||
return Err(bad(
|
||||
C,
|
||||
format!("object {}: Extrude & Bevel (Classic) at index {index} has no lighting presets; set its lights with effect.setParams", 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) {
|
||||
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)));
|
||||
}
|
||||
}
|
||||
let mut options = p.clone();
|
||||
|
||||
@@ -115,3 +115,18 @@ fn lighting_presets_refuse_extrude_and_bevel_classic_and_leave_a_mixed_selection
|
||||
let params = &s.doc().unwrap().doc.node(NodeId(classic)).unwrap().appearance.effects[0].params;
|
||||
assert_eq!((params["lightRotation"].as_f64(), params["lightHeight"].as_f64()), (Some(135.0), Some(45.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lighting_presets_refuse_revolve_classic() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let id = s.execute("path.create", &json!({"d":"M100 40 L130 60 L130 140 L100 160"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("paint.setFill", &json!({"color":"#3366cc"})).unwrap();
|
||||
s.execute("effect.apply", &json!({"effect":"threeD.classicRevolve","item":null,"params":{"surface":"diffuse"}})).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":"topLeft"})).unwrap_err().to_string();
|
||||
assert!(err.contains(&format!("object {id}: Revolve (Classic) at index 0 has no lighting presets")), "{err}");
|
||||
assert_eq!(s.doc().unwrap().doc, before);
|
||||
assert!(!s.doc().unwrap().is_dirty());
|
||||
}
|
||||
|
||||
@@ -341,6 +341,9 @@ impl Revolve {
|
||||
pub struct Face {
|
||||
pub points: Vec<Point>,
|
||||
pub depth: f64,
|
||||
/// The face's unit normal in view space (x right, y down, z toward the viewer), turned to the
|
||||
/// side the viewer sees.
|
||||
pub normal: [f64; 3],
|
||||
pub brightness: f64,
|
||||
pub gradient: Option<ShadeGradient>,
|
||||
/// RGB illumination and its unclipped smooth ramp, independent of the legacy white ramp.
|
||||
@@ -453,6 +456,7 @@ fn project_face(vertices: &[V3], normals: Option<&[V3]>, o: Revolve, origin: Poi
|
||||
Ok(Some(Face {
|
||||
points,
|
||||
depth: vertices.iter().map(|v| v.2).sum::<f64>() / vertices.len() as f64,
|
||||
normal: [normal.0, normal.1, normal.2],
|
||||
brightness,
|
||||
gradient,
|
||||
illumination,
|
||||
@@ -470,6 +474,12 @@ pub fn revolve(path: &PathData, bounds: Rect, options: Revolve) -> Result<Vec<Fa
|
||||
revolve_faces(path, FillRule::NonZero, bounds, options, None)
|
||||
}
|
||||
|
||||
/// [`revolve`]'s faces as facets, back to front, for the classic surfaces: each keeps its
|
||||
/// view-space normal, and its brightness and illumination are those of `options`.
|
||||
pub fn revolve_facets(path: &PathData, bounds: Rect, options: Revolve) -> Result<Vec<Facet>, String> {
|
||||
Ok(revolve(path, bounds, options)?.into_iter().map(Facet::of_face).collect())
|
||||
}
|
||||
|
||||
const TOO_COMPLEX_PROFILE: &str = "Revolve profile is too complex";
|
||||
const TOO_COMPLEX_SURFACE: &str = "Revolve surface is too complex; reduce Segments or simplify the profile";
|
||||
|
||||
|
||||
@@ -294,6 +294,24 @@ impl Facet {
|
||||
}
|
||||
PathData::from_bezpath(&bp)
|
||||
}
|
||||
|
||||
/// Revolve's face `face` as a facet: its polygon, curved outline, depth, normal and lighting.
|
||||
/// A face that joins the profile's ends round the axis is a side; a cap is an end.
|
||||
pub(crate) fn of_face(face: crate::Face) -> Self {
|
||||
Facet {
|
||||
contours: vec![face.points],
|
||||
depth: face.depth,
|
||||
brightness: face.brightness,
|
||||
illumination: face.illumination,
|
||||
normal: face.normal,
|
||||
rgb_gradient: None,
|
||||
outline: face.outline,
|
||||
side: !face.cap,
|
||||
away: false,
|
||||
#[cfg(test)]
|
||||
view: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finite(v: f64, fallback: f64, min: f64, max: f64) -> f64 {
|
||||
|
||||
@@ -94,3 +94,32 @@ fn surfaces_round_trip_through_their_ids() {
|
||||
assert_eq!(ClassicSurface::parse("glossy"), None);
|
||||
assert_eq!(ClassicSurface::default(), ClassicSurface::Plastic);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revolve_facets_keep_each_face_normal_turned_to_the_viewer() {
|
||||
// A cylinder of radius 30 about the left edge, with closed ends, tipped toward the viewer.
|
||||
let mut b = BezPath::new();
|
||||
b.move_to((100.0, 0.0));
|
||||
b.line_to((130.0, 0.0));
|
||||
b.line_to((130.0, 100.0));
|
||||
b.line_to((100.0, 100.0));
|
||||
let path = PathData::from_bezpath(&b);
|
||||
let options = Revolve { rotation_x: -30.0, shade: false, smooth_shading: false, ..Revolve::default() };
|
||||
let faces = revolve(&path, path.bounds().unwrap(), options).unwrap();
|
||||
let facets = revolve_facets(&path, path.bounds().unwrap(), options).unwrap();
|
||||
assert_eq!(facets.len(), faces.len());
|
||||
for (facet, face) in facets.iter().zip(&faces) {
|
||||
let [x, y, z] = facet.normal;
|
||||
assert!(((x * x + y * y + z * z).sqrt() - 1.0).abs() < 1e-9, "unit length: {:?}", facet.normal);
|
||||
assert!(z >= 0.0, "turned to the viewer: {:?}", facet.normal);
|
||||
assert_eq!((facet.depth, facet.side), (face.depth, !face.cap));
|
||||
}
|
||||
// Tipped 30°, the wall faces nearest the viewer turn 30° down from it, and the flat top and bottom
|
||||
// (the profile's horizontal edges swept round) face up and out at 30°.
|
||||
let (s, c) = 30f64.to_radians().sin_cos();
|
||||
assert!(facets.iter().any(|f| f.normal[2] > c - 0.01 && (f.normal[1] - s).abs() < 0.01), "{facets:?}");
|
||||
assert!(facets.iter().any(|f| (f.normal[1] + c).abs() < 1e-6 && (f.normal[2] - s).abs() < 1e-6), "{facets:?}");
|
||||
// Moved off the axis, the profile's open ends close with caps, which are ends rather than sides.
|
||||
let ring = revolve_facets(&path, path.bounds().unwrap(), Revolve { offset: 10.0, ..options }).unwrap();
|
||||
assert!(ring.iter().any(|f| !f.side && (f.normal[1] + c).abs() < 1e-6 && (f.normal[2] - s).abs() < 1e-6));
|
||||
}
|
||||
|
||||
@@ -293,6 +293,7 @@ mod tests {
|
||||
Face {
|
||||
points: vec![Point::new(rect.x0, rect.y0), Point::new(rect.x1, rect.y0), Point::new(rect.x1, rect.y1), Point::new(rect.x0, rect.y1)],
|
||||
depth: brightness,
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
brightness,
|
||||
gradient: None,
|
||||
illumination: [brightness; 3],
|
||||
|
||||
@@ -382,25 +382,27 @@ fn choices(effect: &str, key: &str) -> Option<form::Choices> {
|
||||
|
||||
/// The parameters whose dialogs list their values in their own bodies, as (label, value), each
|
||||
/// label as that dialog passes it to its dropdown or radio buttons, which translate it into the UI
|
||||
/// language: Revolve's edge, the bevel of both Extrude & Bevels (Rounded translated in the "3d"
|
||||
/// context first), Extrude & Bevel (Classic)'s surface and shading color, every Warp style's
|
||||
/// language: the edge of both Revolves, the bevel of both Extrude & Bevels (Rounded translated in
|
||||
/// the "3d" context first), the surface and shading color of the classic 3D effects, every Warp style's
|
||||
/// orientation, Rasterize's Color Model (its `document` value labeled
|
||||
/// CMYK when `cmyk`, for a document in CMYK, and RGB otherwise), Background and Anti-aliasing, and
|
||||
/// the channel of Levels and Curves.
|
||||
pub(super) fn own_choices(effect: &str, key: &str, cmyk: bool) -> Option<Vec<(&'static str, &'static str)>> {
|
||||
let c3 = |s: &'static str| crate::i18n::tr_ctx(crate::i18n::current(), "3d", s);
|
||||
Some(match (effect, key) {
|
||||
(vectorcraft_effects::REVOLVE, "edge") => vec![(tl!("Left"), "left"), (tl!("Right"), "right")],
|
||||
(vectorcraft_effects::REVOLVE | vectorcraft_effects::CLASSIC_REVOLVE, "edge") => vec![(tl!("Left"), "left"), (tl!("Right"), "right")],
|
||||
(
|
||||
vectorcraft_effects::REVOLVE | vectorcraft_effects::EXTRUDE | vectorcraft_effects::INFLATE | vectorcraft_effects::ROTATE,
|
||||
"shadowPosition",
|
||||
) => {
|
||||
vec![(tl!("Behind Object"), "behind"), (tl!("Below Object"), "below")]
|
||||
}
|
||||
(vectorcraft_effects::CLASSIC_EXTRUDE, "surface") => {
|
||||
(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, "shadingColor") => vec![(tl!("Black"), "black"), (tl!("Custom"), "custom"), (tl!("None"), "none")],
|
||||
(vectorcraft_effects::CLASSIC_EXTRUDE | vectorcraft_effects::CLASSIC_REVOLVE, "shadingColor") => {
|
||||
vec![(tl!("Black"), "black"), (tl!("Custom"), "custom"), (tl!("None"), "none")]
|
||||
}
|
||||
(vectorcraft_effects::EXTRUDE | vectorcraft_effects::CLASSIC_EXTRUDE, "bevel") => vec![
|
||||
(tl!("None"), "none"),
|
||||
(tl!("Classic"), "classic"),
|
||||
@@ -965,6 +967,9 @@ mod tests {
|
||||
(vectorcraft_effects::CLASSIC_EXTRUDE, "bevel"),
|
||||
(vectorcraft_effects::CLASSIC_EXTRUDE, "surface"),
|
||||
(vectorcraft_effects::CLASSIC_EXTRUDE, "shadingColor"),
|
||||
(vectorcraft_effects::CLASSIC_REVOLVE, "edge"),
|
||||
(vectorcraft_effects::CLASSIC_REVOLVE, "surface"),
|
||||
(vectorcraft_effects::CLASSIC_REVOLVE, "shadingColor"),
|
||||
("warp.arc", "orientation"),
|
||||
("warp.twist", "orientation"),
|
||||
(vectorcraft_effects::RASTERIZE, "background"),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
//! Compact controls for the 3D and Materials effects: Extrude & Bevel, Revolve, Inflate and Rotate,
|
||||
//! and Extrude & Bevel (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
|
||||
//! and Extrude & Bevel (Classic) and Revolve (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.
|
||||
|
||||
use egui::{Align2, FontId, Id, Pos2, Sense, Stroke, vec2};
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::Unit;
|
||||
use vectorcraft_effects::{CLASSIC_EXTRUDE, EXTRUDE, INFLATE, REVOLVE, ROTATION_PRESETS};
|
||||
use vectorcraft_effects::{CLASSIC_EXTRUDE, CLASSIC_REVOLVE, EXTRUDE, INFLATE, REVOLVE, ROTATION_PRESETS};
|
||||
|
||||
use crate::state::Dialog;
|
||||
use crate::theme::Tokens;
|
||||
@@ -145,11 +145,12 @@ pub(super) fn c3(s: &'static str) -> &'static str {
|
||||
crate::i18n::tr_ctx(crate::i18n::current(), "3d", s)
|
||||
}
|
||||
|
||||
/// Extrude & Bevel, Inflate and Extrude & Bevel (Classic): Object, Rotation and Lighting tabs
|
||||
/// (Rotate has no Object tab). The rotation gizmo shows on the first two, the light gizmo on
|
||||
/// Lighting.
|
||||
/// 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.
|
||||
fn solid_controls(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, id: &str) {
|
||||
let object = id == EXTRUDE || id == INFLATE || id == CLASSIC_EXTRUDE;
|
||||
let classic = id == CLASSIC_EXTRUDE || id == CLASSIC_REVOLVE;
|
||||
let object = id == EXTRUDE || id == INFLATE || classic;
|
||||
let mut tabs = vec![];
|
||||
if object {
|
||||
tabs.push(tl!("Object"));
|
||||
@@ -169,7 +170,7 @@ fn solid_controls(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, id: &
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
if super::revolve_gizmo::lighting(d) {
|
||||
if id == CLASSIC_EXTRUDE {
|
||||
if classic {
|
||||
super::three_d_classic::surface_controls(ui, d);
|
||||
} else {
|
||||
lighting_controls(ui, d);
|
||||
@@ -178,6 +179,8 @@ fn solid_controls(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, id: &
|
||||
let unit = app.session.general_unit();
|
||||
if id == CLASSIC_EXTRUDE {
|
||||
super::three_d_classic::object_controls(ui, d, unit);
|
||||
} else if id == CLASSIC_REVOLVE {
|
||||
super::three_d_classic::revolve_controls(ui, d, unit);
|
||||
} else if id == EXTRUDE {
|
||||
extrude_controls(ui, d, unit);
|
||||
} else {
|
||||
|
||||
@@ -379,6 +379,49 @@ fn classic_extrude_bevel_shows_its_options_and_applies_as_one_step() {
|
||||
assert_eq!(h.app.session.doc().unwrap().doc, before);
|
||||
}
|
||||
|
||||
/// Revolve (Classic): its angle, cap, offset and axis side on the Object tab, Position on the
|
||||
/// Rotation tab, and the classic surface on the Lighting tab, without Draw Hidden Faces; OK keeps
|
||||
/// the edit as one undo step.
|
||||
#[test]
|
||||
fn classic_revolve_shows_its_options_and_applies_as_one_step() {
|
||||
use vectorcraft_effects::CLASSIC_REVOLVE;
|
||||
let mut h = Harness::with("M100 40 L130 60 L130 140 L100 160", CLASSIC_REVOLVE, vec2(1400.0, 1000.0));
|
||||
let before = h.app.session.doc().unwrap().doc.clone();
|
||||
h.settle();
|
||||
for text in ["Angle", "Cap", "Offset", "Axis side", "Left"] {
|
||||
h.rect(text);
|
||||
}
|
||||
assert!(!h.texts.iter().any(|(t, _)| t == "Segments" || t == "Revolve angle" || t == "Depth"), "the other effects' options stay out");
|
||||
h.app.ui.dialog.as_mut().unwrap().fields.insert("__tab".into(), json!("rotation"));
|
||||
h.settle();
|
||||
for text in ["Presets", "Rotation X", "Perspective", "Off-Axis Front"] {
|
||||
h.rect(text);
|
||||
}
|
||||
h.click(h.rect("Lighting").center());
|
||||
h.settle();
|
||||
for text in ["Surface", "Plastic", "Light Intensity", "Ambient Light", "Highlight Size", "Blend Steps", "Shading Color"] {
|
||||
h.rect(text);
|
||||
}
|
||||
assert!(!h.texts.iter().any(|(t, _)| t == "Draw Hidden Faces"), "every face is drawn");
|
||||
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("edge".into(), json!("right"));
|
||||
d.fields.insert("offset".into(), json!(12.0));
|
||||
d.fields.insert("surface".into(), json!("wireframe"));
|
||||
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_REVOLVE);
|
||||
assert_eq!(
|
||||
(fx.params["edge"].clone(), fx.params["offset"].as_f64(), fx.params["surface"].clone()),
|
||||
(json!("right"), Some(12.0), json!("wireframe"))
|
||||
);
|
||||
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,9 +1,9 @@
|
||||
//! Artboard rotation and light controls for the active 3D effect dialog (Extrude & Bevel, Revolve,
|
||||
//! Inflate, Rotate). Gestures edit fields;
|
||||
//! Inflate, Rotate and the classic 3D effects). Gestures edit fields;
|
||||
//! the existing effect preview applies commands and owns OK, Cancel and undo.
|
||||
use egui::{Align2, FontId, Id, Pos2, Rect, Sense, Stroke, Vec2, vec2};
|
||||
use serde_json::json;
|
||||
use vectorcraft_effects::REVOLVE;
|
||||
use vectorcraft_effects::{CLASSIC_REVOLVE, REVOLVE};
|
||||
use vectorcraft_geom::Point;
|
||||
use vectorcraft_three_d::Revolve;
|
||||
|
||||
@@ -20,10 +20,11 @@ pub(crate) fn lighting(d: &Dialog) -> bool {
|
||||
d.str("__gizmo") == "light"
|
||||
}
|
||||
|
||||
/// Does the effect take light: Shading on, or for Extrude & Bevel (Classic) a Diffuse or Plastic
|
||||
/// Does the effect take light: Shading on, or for the classic 3D effects a Diffuse or Plastic
|
||||
/// surface?
|
||||
fn lit(d: &Dialog) -> bool {
|
||||
if d.str("__effect") == vectorcraft_effects::CLASSIC_EXTRUDE {
|
||||
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")
|
||||
@@ -97,8 +98,9 @@ impl Geometry {
|
||||
let bounds = node.geometric_bounds()?;
|
||||
let xf = Xf::new(app.canvas_rect?, app.view()?);
|
||||
let options = super::revolve::light_options(d);
|
||||
// Revolve turns about its axis; the others about the centre of the art's bounds.
|
||||
let origin = if d.str("__effect") == REVOLVE { options.rotation_origin(bounds) } else { bounds.center() };
|
||||
// Both Revolves turn about their axis; the others about the centre of the art's bounds.
|
||||
let id = d.str("__effect");
|
||||
let origin = if id == REVOLVE || id == CLASSIC_REVOLVE { options.rotation_origin(bounds) } else { bounds.center() };
|
||||
let center = xf.to_screen(origin);
|
||||
if !center.x.is_finite() || !center.y.is_finite() || !xf.rect.contains(center) {
|
||||
return None;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
//! Extrude & Bevel (Classic)'s own controls in the compact 3D dialog: the extrude and bevel 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) 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.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::Unit;
|
||||
use vectorcraft_effects::CLASSIC_EXTRUDE;
|
||||
use vectorcraft_effects::{CLASSIC_EXTRUDE, CLASSIC_REVOLVE};
|
||||
|
||||
use super::revolve::{c3, choice, heading, label, light_list, light_record, number, reset_dot, selected_light, store_selected, toggle};
|
||||
use crate::state::Dialog;
|
||||
@@ -25,8 +25,23 @@ pub(super) fn object_controls(ui: &mut egui::Ui, d: &mut Dialog, unit: Unit) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Revolve (Classic)'s angle, cap and offset, and the edge of the profile's bounds the axis is at.
|
||||
pub(super) fn revolve_controls(ui: &mut egui::Ui, d: &mut Dialog, unit: Unit) {
|
||||
heading(ui, tl!("Object"));
|
||||
number(ui, d, "angle", tl!("Angle"), (0.0, 360.0), "°", 1, None);
|
||||
choice(ui, d, "cap", c3("Cap"), &[(json!(true), c3("Solid")), (json!(false), c3("Hollow"))]);
|
||||
number(ui, d, "offset", tl!("Offset"), (0.0, 1000.0), "", 1, Some(unit));
|
||||
let sides: Vec<(Value, &str)> = super::effect::own_choices(CLASSIC_REVOLVE, "edge", false)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|(label, value)| (json!(value), label))
|
||||
.collect();
|
||||
choice(ui, d, "edge", tl!("Axis side"), &sides);
|
||||
}
|
||||
|
||||
/// The surface, then for Diffuse and Plastic the lights, Ambient Light, the highlight (Plastic
|
||||
/// only), Blend Steps and Shading Color; Draw Hidden Faces last.
|
||||
/// only), Blend Steps and Shading Color; Draw Hidden Faces last, for Extrude & Bevel (Classic)
|
||||
/// (Revolve (Classic) draws every face).
|
||||
pub(super) fn surface_controls(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
choice(ui, d, "surface", tl!("Surface"), &options("surface"));
|
||||
let surface = d.str("surface");
|
||||
@@ -52,7 +67,9 @@ pub(super) fn surface_controls(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
shading_custom(ui, d);
|
||||
}
|
||||
});
|
||||
toggle(ui, d, "drawHiddenFaces", tl!("Draw Hidden Faces"), false);
|
||||
if d.str("__effect") == 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).
|
||||
|
||||
@@ -1119,6 +1119,7 @@ font-technology Variable Променливи
|
||||
Extrude & Bevel… Избутване и фасетка…
|
||||
3D (Classic) 3D (класически)
|
||||
Extrude & Bevel (Classic)… Избутване и фасетка (класически)…
|
||||
Revolve (Classic)… Завъртане (класическо)…
|
||||
Surface Повърхност
|
||||
Wireframe Телена рамка
|
||||
No Shading Без засенчване
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -483,6 +483,7 @@
|
||||
Extrude & Bevel… Istiskivanje i zakošenje…
|
||||
3D (Classic) 3D (klasično)
|
||||
Extrude & Bevel (Classic)… Istiskivanje i zakošenje (klasično)…
|
||||
Revolve (Classic)… Rotaciono tijelo (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.
|
@@ -1573,6 +1573,7 @@ antiAlias Sharp Nítid
|
||||
Extrude & Bevel… Extrudeix i bisella…
|
||||
3D (Classic) 3D (clàssic)
|
||||
Extrude & Bevel (Classic)… Extrudeix i bisella (clàssic)…
|
||||
Revolve (Classic)… Revoluciona (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.
|
@@ -492,6 +492,7 @@
|
||||
Extrude & Bevel… Vysunutí a zkosení…
|
||||
3D (Classic) 3D (klasické)
|
||||
Extrude & Bevel (Classic)… Vysunutí a zkosení (klasické)…
|
||||
Revolve (Classic)… Rotační těleso (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.
|
@@ -450,6 +450,7 @@
|
||||
Extrude & Bevel… Ekstrudér og facet…
|
||||
3D (Classic) 3D (klassisk)
|
||||
Extrude & Bevel (Classic)… Ekstrudér og facet (klassisk)…
|
||||
Revolve (Classic)… Omdrej (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.
|
@@ -1124,6 +1124,7 @@ font-technology Variable Variabel
|
||||
Extrude & Bevel… Extrudieren und abgeflachte Kante…
|
||||
3D (Classic) 3D (klassisch)
|
||||
Extrude & Bevel (Classic)… Extrudieren und abgeflachte Kante (klassisch)…
|
||||
Revolve (Classic)… Kreiseln (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.
|
@@ -1097,6 +1097,7 @@ font-technology Variable Μεταβλητές
|
||||
Extrude & Bevel… Εξώθηση και λοξότμηση…
|
||||
3D (Classic) 3D (κλασικό)
|
||||
Extrude & Bevel (Classic)… Εξώθηση και λοξότμηση (κλασικό)…
|
||||
Revolve (Classic)… Περιστροφή γύρω από άξονα (κλασικό)…
|
||||
Surface Επιφάνεια
|
||||
Wireframe Συρματόπλεγμα
|
||||
No Shading Χωρίς σκίαση
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1153,6 +1153,7 @@ font-technology Variable Variables
|
||||
Extrude & Bevel… Extrusión y biselado…
|
||||
3D (Classic) 3D (clásico)
|
||||
Extrude & Bevel (Classic)… Extrusión y biselado (clásico)…
|
||||
Revolve (Classic)… Revolución (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.
|
@@ -489,6 +489,7 @@
|
||||
Extrude & Bevel… Pursota ja viistä…
|
||||
3D (Classic) 3D (perinteinen)
|
||||
Extrude & Bevel (Classic)… Pursota ja viistä (perinteinen)…
|
||||
Revolve (Classic)… Pyöräytä (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.
|
@@ -1148,6 +1148,7 @@ font-technology Variable Variables
|
||||
Extrude & Bevel… Extrusion et biseautage…
|
||||
3D (Classic) 3D (classique)
|
||||
Extrude & Bevel (Classic)… Extrusion et biseautage (classique)…
|
||||
Revolve (Classic)… Révolution (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.
|
@@ -1745,6 +1745,7 @@ antiAlias Sharp शार्प
|
||||
Extrude & Bevel… एक्सट्रूडेड और बेवेल…
|
||||
3D (Classic) 3D (क्लासिक)
|
||||
Extrude & Bevel (Classic)… एक्सट्रूडेड और बेवेल (क्लासिक)…
|
||||
Revolve (Classic)… रिवॉल्व (क्लासिक)…
|
||||
Surface सतह
|
||||
Wireframe वायरफ़्रेम
|
||||
No Shading कोई शेडिंग नहीं
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1444,6 +1444,7 @@ font-technology Variable Változó
|
||||
Extrude & Bevel… Kihúzás és élletörés…
|
||||
3D (Classic) 3D (klasszikus)
|
||||
Extrude & Bevel (Classic)… Kihúzás és élletörés (klasszikus)…
|
||||
Revolve (Classic)… Forgatás tengely körül (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.
|
@@ -450,6 +450,7 @@
|
||||
Extrude & Bevel… Ekstrusi & bevel…
|
||||
3D (Classic) 3D (klasik)
|
||||
Extrude & Bevel (Classic)… Ekstrusi & bevel (klasik)…
|
||||
Revolve (Classic)… Revolusi (klasik)…
|
||||
Surface Permukaan
|
||||
Wireframe Kerangka kawat
|
||||
No Shading Tanpa pengarsiran
|
||||
|
||||
|
@@ -1154,6 +1154,7 @@ font-technology Variable Variabili
|
||||
Extrude & Bevel… Estrusione e smusso…
|
||||
3D (Classic) 3D (classico)
|
||||
Extrude & Bevel (Classic)… Estrusione e smusso (classico)…
|
||||
Revolve (Classic)… Rivoluzione (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.
|
@@ -527,6 +527,7 @@ digits Malayalam マラヤーラム数字
|
||||
Extrude & Bevel… 押し出しとベベル…
|
||||
3D (Classic) 3D(クラシック)
|
||||
Extrude & Bevel (Classic)… 押し出しとベベル(クラシック)…
|
||||
Revolve (Classic)… 回転体(クラシック)…
|
||||
Surface 表面
|
||||
Wireframe ワイヤーフレーム
|
||||
No Shading 陰影なし
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -418,6 +418,7 @@
|
||||
Extrude & Bevel… ამოწევა და წაკვეთა…
|
||||
3D (Classic) 3D (კლასიკური)
|
||||
Extrude & Bevel (Classic)… ამოწევა და წაკვეთა (კლასიკური)…
|
||||
Revolve (Classic)… ბრუნვა (კლასიკური)…
|
||||
Surface ზედაპირი
|
||||
Wireframe მავთულის ჩარჩო
|
||||
No Shading დაჩრდილვის გარეშე
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -520,6 +520,7 @@ digits Malayalam 말라얄람어
|
||||
Extrude & Bevel… 돌출 및 경사…
|
||||
3D (Classic) 3D(클래식)
|
||||
Extrude & Bevel (Classic)… 돌출 및 경사(클래식)…
|
||||
Revolve (Classic)… 회전체(클래식)…
|
||||
Surface 표면
|
||||
Wireframe 와이어프레임
|
||||
No Shading 셰이딩 없음
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -464,6 +464,7 @@
|
||||
Extrude & Bevel… Extruderen en afschuinen…
|
||||
3D (Classic) 3D (klassiek)
|
||||
Extrude & Bevel (Classic)… Extruderen en afschuinen (klassiek)…
|
||||
Revolve (Classic)… Draaien (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.
|
@@ -451,6 +451,7 @@
|
||||
Extrude & Bevel… Wyciągnięcie i fazowanie…
|
||||
3D (Classic) 3D (klasyczne)
|
||||
Extrude & Bevel (Classic)… Wyciągnięcie i fazowanie (klasyczne)…
|
||||
Revolve (Classic)… Obrót (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.
|
@@ -1046,6 +1046,7 @@ antiAlias Sharp Nítido
|
||||
Extrude & Bevel… Extrusão e Bisel…
|
||||
3D (Classic) 3D (Clássico)
|
||||
Extrude & Bevel (Classic)… Extrusão e Bisel (Clássico)…
|
||||
Revolve (Classic)… Revolver (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.
|
@@ -1151,6 +1151,7 @@ font-technology Variable Вариативные
|
||||
Extrude & Bevel… Выдавливание и фаска…
|
||||
3D (Classic) 3D (классические)
|
||||
Extrude & Bevel (Classic)… Выдавливание и фаска (классическое)…
|
||||
Revolve (Classic)… Вращение (классическое)…
|
||||
Surface Поверхность
|
||||
Wireframe Каркас
|
||||
No Shading Без затенения
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1132,6 +1132,7 @@ font-technology Variable Variabla
|
||||
Extrude & Bevel… Extrudera och fasa…
|
||||
3D (Classic) 3D (klassisk)
|
||||
Extrude & Bevel (Classic)… Extrudera och fasa (klassisk)…
|
||||
Revolve (Classic)… Rotera runt axel (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.
|
@@ -1124,6 +1124,7 @@ font-technology Variable Değişken
|
||||
Extrude & Bevel… Ekstrüzyon ve Eğim…
|
||||
3D (Classic) 3D (Klasik)
|
||||
Extrude & Bevel (Classic)… Ekstrüzyon ve Eğim (Klasik)…
|
||||
Revolve (Classic)… Döndür (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.
|
@@ -1148,6 +1148,7 @@ font-technology Variable Варіативні
|
||||
Extrude & Bevel… Видавлювання та фаска…
|
||||
3D (Classic) 3D (класичні)
|
||||
Extrude & Bevel (Classic)… Видавлювання та фаска (класичне)…
|
||||
Revolve (Classic)… Обертання (класичне)…
|
||||
Surface Поверхня
|
||||
Wireframe Каркас
|
||||
No Shading Без затінення
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 182 and column 5.
|
@@ -1321,6 +1321,7 @@ font-technology Variable Biến thiên
|
||||
Extrude & Bevel… Kéo dài & vát…
|
||||
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)…
|
||||
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.
|
@@ -1094,6 +1094,7 @@ antiAlias Sharp 锐利
|
||||
Extrude & Bevel… 突出与斜角…
|
||||
3D (Classic) 3D (经典)
|
||||
Extrude & Bevel (Classic)… 突出与斜角 (经典)…
|
||||
Revolve (Classic)… 回转 (经典)…
|
||||
Surface 表面
|
||||
Wireframe 线框
|
||||
No Shading 无底纹
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1118,6 +1118,7 @@ font-technology Variable 可變
|
||||
Extrude & Bevel… 突出與斜角…
|
||||
3D (Classic) 3D (經典)
|
||||
Extrude & Bevel (Classic)… 突出與斜角 (經典)…
|
||||
Revolve (Classic)… 迴轉 (經典)…
|
||||
Surface 表面
|
||||
Wireframe 線框
|
||||
No Shading 無網底
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -524,6 +524,22 @@ fn classic_extrude_bevel_is_in_the_3d_classic_submenu() {
|
||||
assert!(at("Materials…").unwrap() < at("Extrude & Bevel (Classic)…").unwrap());
|
||||
}
|
||||
|
||||
/// Effect › 3D and Materials › 3D (Classic) › Revolve (Classic)… opens its effect dialog, after
|
||||
/// Extrude & Bevel (Classic)…, and nowhere else.
|
||||
#[test]
|
||||
fn classic_revolve_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.classicRevolve")).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()), ("Revolve (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("Extrude & Bevel (Classic)…").unwrap() < at("Revolve (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]
|
||||
|
||||
@@ -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), 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) and 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 (Revolve, Rotate, Map Art), Turntable) is 25–45 h.
|
||||
angular cut caps, exact visibility, the rest of 3D Classic (Rotate, Map Art), Turntable) is 25–45 h.
|
||||
|
||||
## Revision history
|
||||
|
||||
|
||||
+1
-1
@@ -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 (Revolve, Rotate, Map Art), Turntable, textures, map art and shading in 3D export.
|
||||
Classic (Rotate, 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.
|
||||
|
||||
|
||||
+25
-3
@@ -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) has no lighting presets: a target with it 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) 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.
|
||||
|
||||
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) has no shadow. See [three-d.md](three-d.md#shadows).
|
||||
it as a path. Extrude & Bevel (Classic) and Revolve (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}}}}
|
||||
@@ -3187,6 +3187,28 @@ an unknown surface or shading color is refused with an error and the document is
|
||||
{"name":"run_command","arguments":{"command":"effect.apply","params":{"effect":"threeD.classicExtrude","item":null,"params":{"depth":40,"bevel":"rounded","bevelHeight":6,"surface":"diffuse","blendSteps":64}}}}
|
||||
```
|
||||
|
||||
## Revolve (Classic)
|
||||
|
||||
`effect.apply {effect: "threeD.classicRevolve", params: {...}, ids?, item?}` is Effect › 3D and Materials ›
|
||||
3D (Classic) › Revolve (Classic)…. It sweeps the profile round a vertical axis as `threeD.revolve` does, at 64 segments
|
||||
for a full turn, and draws every face, back to front, with the classic surfaces. Parameters: `angle` (degrees,
|
||||
0..360, default 360), `cap` (true closes the profile's open ends, false leaves the surface hollow), `offset` (points,
|
||||
0..1000, default 0; the axis's distance from the profile, so a value above 0 makes a ring), `edge` (`left`, the
|
||||
default, or `right`: the edge of the profile's bounds the axis is at), `rotationX/Y/Z` and `perspectiveAngle` as
|
||||
above, and `surface`, the lights, `ambient`, `highlightIntensity`, `highlightSize`, `blendSteps`, `shadingColor` and
|
||||
`shadingCustom` as Extrude & Bevel (Classic) takes them. It has no `drawHiddenFaces`. The effect also goes on a group
|
||||
or layer, whose members turn about one shared axis, or on a fill or stroke. The public documentation describes the
|
||||
dialog ([Create 3D objects](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/create-3d-objects.html))
|
||||
and its Angle (0 to 360), Cap, Offset (0 to 1000) and Left Edge or Right Edge settings
|
||||
([Revolve options](https://helpx.adobe.com/pdf/cs6/illustrator_reference.pdf#page=283));
|
||||
the defaults are VectorCraft's own. An unknown parameter (`segments` included), a number out of range, a flag that
|
||||
is not true or false, another `edge`, surface or shading color is refused with an error and the document is not
|
||||
changed.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"effect.apply","params":{"effect":"threeD.classicRevolve","item":null,"params":{"angle":270,"offset":10,"surface":"diffuse","blendSteps":64}}}}
|
||||
```
|
||||
|
||||
|
||||
## Edit Colors and Recolor Artwork
|
||||
|
||||
|
||||
+6
-4
@@ -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 and Revolve) also applies to a clipping
|
||||
Every 3D effect (Extrude & Bevel, Extrude & Bevel (Classic), Inflate, Rotate, 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) has no shadow.
|
||||
path below the faces. Extrude & Bevel (Classic) and Revolve (Classic) have no shadow.
|
||||
|
||||
## Parameters (`effect.apply`, `effect.setParams`)
|
||||
|
||||
@@ -336,8 +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) is built, with its surface types and blend steps. Revolve (Classic),
|
||||
Rotate (Classic), the track cube, map art, Turntable and 3D export are separate, unbuilt items.
|
||||
- **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
|
||||
are separate, unbuilt items.
|
||||
|
||||
Release measurements and a repeatable CLI scene are in [3D lighting performance](3d-lighting-performance.md).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user