mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 02:19:56 -06:00
3D (Classic): Extrude & Bevel (Classic) extrudes art with Wireframe, No Shading, Diffuse and Plastic surfaces
Effect > 3D and Materials > 3D (Classic) > Extrude & Bevel (Classic)... applies `threeD.classicExtrude`, a live effect built on the Extrude & Bevel solid. It takes Extrude Depth, Cap, a bevel with Height and Bevel Extent In or Out, the Position presets, rotation and Perspective, and draws the faces with the Wireframe, No Shading, Diffuse or Plastic surface: one to ten lights, Ambient Light, Highlight Intensity and Size, Blend Steps, a Black, Custom or no Shading Color, and Draw Hidden Faces. Invalid parameters are refused with an error.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use super::{CROP_MARKS, EXTRUDE, INFLATE, PATHFINDER_EFFECTS, RASTERIZE, REVOLVE, ROTATE, TRAP, Value, json, trap};
|
||||
use super::{CLASSIC_EXTRUDE, CROP_MARKS, EXTRUDE, INFLATE, PATHFINDER_EFFECTS, RASTERIZE, REVOLVE, ROTATE, TRAP, Value, json, trap};
|
||||
|
||||
/// Catalogue entry for one effect.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -80,6 +80,7 @@ fn lengths_of(id: &str) -> Lengths {
|
||||
"path.offsetPath" => always(&["offset"]),
|
||||
"threeD.revolve" => always(&["offset"]),
|
||||
"threeD.extrude" => always(&["depth", "bevelWidth", "bevelHeight"]),
|
||||
"threeD.classicExtrude" => always(&["depth", "bevelHeight"]),
|
||||
"threeD.inflate" => always(&["depth"]),
|
||||
"path.outlineStroke" => always(&["width"]),
|
||||
"stylize.roundCorners" | "stylize.feather" | "blur.gaussian" | "blur.smart" | "sharpen.unsharpMask" => always(&["radius"]),
|
||||
@@ -198,6 +199,13 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
|
||||
"{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), perspective: % (0..100; the earlier form, read as the perspectiveAngle that shows the same view), shade: bool (true), lightColor: #rrggbb (white, #ffffff; legacy directional light color), lights: null or array of 1..10 objects (color: #rrggbb, intensity: % 0..100, rotation: degrees -180..180, height: degrees 0..90, softness: % 0..100, behind: bool; overrides the single-light fields; ambient is shared), lightRotation: degrees (-180..180, 125.26; round the object on the page, 0 on its right, counterclockwise), lightHeight: degrees (0..90, 30; 0 beside the object, 90 straight in front), lightBehind: bool (false; true lights it from behind), lightIntensity: % (0..100, 80), lightSoftness: % (0..100, 0; how far round the object the light reaches), ambient: % (0..200, 25; Ambient Light), lightAzimuth: degrees (the earlier form: with lightElevation, read as the lightRotation, lightHeight and lightBehind of the same light), lightElevation: degrees (the earlier form, with lightAzimuth)}. Turns the flat art in 3D about the centre of its bounds; every fill and stroke keeps its paint, shaded as one plane.",
|
||||
json!({"rotationX":-21.88,"rotationY":-23.4,"rotationZ":9.06,"perspectiveAngle":0.0,"shade":true,"lightColor":"#ffffff","lights":null,"lightRotation":125.26438968275465,"lightHeight":30.0,"lightBehind":false,"lightIntensity":80.0,"lightSoftness":0.0,"ambient":25.0}),
|
||||
),
|
||||
g(
|
||||
CLASSIC_EXTRUDE,
|
||||
"Extrude & Bevel (Classic)…",
|
||||
&["Effect", "3D and Materials", "3D (Classic)"],
|
||||
"{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(
|
||||
"convertToShape.rectangle",
|
||||
"Rectangle…",
|
||||
|
||||
@@ -84,8 +84,8 @@ pub use reshape::{
|
||||
};
|
||||
pub use revolve::{REVOLVE, has_revolve, revolve_art, revolve_options, validate_revolve};
|
||||
pub use solids::{
|
||||
EXTRUDE, INFLATE, ROTATE, ROTATION_PRESETS, SOLIDS, extrude_options, has_solid, inflate_options, is_three_d, solid_art, validate_solid,
|
||||
validate_three_d, view_options,
|
||||
CLASSIC_EXTRUDE, EXTRUDE, INFLATE, ROTATE, ROTATION_PRESETS, SHADING_COLORS, SOLIDS, classic_extrude_options, classic_shading, extrude_options,
|
||||
has_solid, inflate_options, is_three_d, solid_art, validate_solid, validate_three_d, view_options,
|
||||
};
|
||||
pub use stylize::{ScribbleSetting, scribble_setting_id, scribble_settings};
|
||||
pub use trap::TRAP;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Extrude & Bevel, Inflate and Rotate 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.
|
||||
//! Extrude & Bevel, Inflate, Rotate and Extrude & Bevel (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;
|
||||
|
||||
use serde_json::Value;
|
||||
@@ -14,11 +14,14 @@ use vectorcraft_doc::color::Color;
|
||||
use crate::util::{flag, num, text};
|
||||
use crate::{GeomContext, apply_geometry_with, is_geometry};
|
||||
|
||||
mod classic;
|
||||
pub use classic::{CLASSIC_EXTRUDE, SHADING_COLORS, classic_extrude_options, classic_shading};
|
||||
|
||||
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; 3] = [EXTRUDE, INFLATE, ROTATE];
|
||||
pub const SOLIDS: [&str; 4] = [EXTRUDE, INFLATE, ROTATE, CLASSIC_EXTRUDE];
|
||||
|
||||
/// Is `id` one of the 3D and Materials effects (Revolve included)?
|
||||
pub fn is_three_d(id: &str) -> bool {
|
||||
@@ -93,6 +96,7 @@ pub fn extrude_options(p: &Value) -> Extrude {
|
||||
inside: flag(p, "bevelInside", false),
|
||||
both_sides: flag(p, "bevelBothSides", false),
|
||||
}),
|
||||
hidden: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +150,7 @@ fn facets(id: &str, path: &PathData, rule: FillRule, bounds: Rect, p: &Value) ->
|
||||
let view = view_options(p);
|
||||
match id {
|
||||
EXTRUDE => vectorcraft_three_d::extrude(path, rule, bounds, extrude_options(p), view),
|
||||
CLASSIC_EXTRUDE => vectorcraft_three_d::extrude(path, rule, bounds, classic_extrude_options(p), view),
|
||||
INFLATE => vectorcraft_three_d::inflate(path, rule, bounds, inflate_options(p), view),
|
||||
_ => vectorcraft_three_d::plane(path, rule, bounds, view),
|
||||
}
|
||||
@@ -153,6 +158,9 @@ fn facets(id: &str, path: &PathData, rule: FillRule, bounds: Rect, p: &Value) ->
|
||||
|
||||
/// Validate before a command changes the document; the evaluator bounds loaded files too.
|
||||
pub fn validate_solid(n: &Node, id: &str, p: &Value) -> Result<(), String> {
|
||||
if id == CLASSIC_EXTRUDE {
|
||||
classic::validate(p)?;
|
||||
}
|
||||
crate::lighting::validate(p)?;
|
||||
let label = crate::effect_info(id).map_or("This effect", |e| e.label.trim_end_matches('…'));
|
||||
if let Some(v) = p.get("bevel")
|
||||
@@ -322,7 +330,8 @@ fn region(path: &PathData, rule: FillRule, item: &AppearanceItem) -> (PathData,
|
||||
fn art_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 colored = view_options(&p).has_colored_lights();
|
||||
let classic = fx.id == CLASSIC_EXTRUDE;
|
||||
let colored = !classic && view_options(&p).has_colored_lights();
|
||||
if fx.id == ROTATE {
|
||||
// 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.
|
||||
@@ -383,7 +392,8 @@ fn art_for(n: &Node, fx: &Effect, shapes: &[Profile]) -> Option<Vec<Arc<Node>>>
|
||||
.map(|(item, member, f)| {
|
||||
let depths: Vec<f64> = f.iter().map(|f| f.depth).collect();
|
||||
let (paint, opacity, blend) = paint_of(item, member);
|
||||
(depths, 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) };
|
||||
(depths, art)
|
||||
})
|
||||
.collect();
|
||||
// One order takes a plain group of faces from each solid, one face for each facet. With
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
//! 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.
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::Value;
|
||||
use vectorcraft_doc::color::{BlendMode, Color, Paint};
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, FillLayer, Node, StrokeLayer};
|
||||
use vectorcraft_geom::Rect;
|
||||
use vectorcraft_three_d::{Bevel, BevelShape, ClassicShading, ClassicSurface, Extrude, Facet, Light};
|
||||
|
||||
use crate::util::{flag, num, text};
|
||||
|
||||
pub const CLASSIC_EXTRUDE: &str = "threeD.classicExtrude";
|
||||
|
||||
/// The width of a Wireframe edge, in points.
|
||||
const WIRE: f64 = 0.5;
|
||||
|
||||
/// The Shading Color choices: black, a custom color, or none.
|
||||
pub const SHADING_COLORS: [&str; 3] = ["black", "custom", "none"];
|
||||
|
||||
/// The surface settings of Extrude & Bevel (Classic)'s parameters.
|
||||
pub fn classic_shading(p: &Value) -> ClassicShading {
|
||||
let d = ClassicShading::default();
|
||||
ClassicShading {
|
||||
surface: ClassicSurface::parse(text(p, "surface", "plastic")).unwrap_or_default(),
|
||||
ambient: num(p, "ambient", d.ambient * 100.0) / 100.0,
|
||||
highlight_intensity: num(p, "highlightIntensity", d.highlight_intensity * 100.0) / 100.0,
|
||||
highlight_size: num(p, "highlightSize", d.highlight_size * 100.0) / 100.0,
|
||||
blend_steps: num(p, "blendSteps", f64::from(d.blend_steps)).round().clamp(1.0, 256.0) as u32,
|
||||
}
|
||||
}
|
||||
|
||||
/// The solid of Extrude & Bevel (Classic)'s parameters: its depth, cap and bevel (as tall as it is
|
||||
/// wide, added around the shape or carved into it), with the faces turned away from the viewer kept
|
||||
/// for Draw Hidden Faces and for the Wireframe surface.
|
||||
pub fn classic_extrude_options(p: &Value) -> Extrude {
|
||||
let height = num(p, "bevelHeight", 4.0);
|
||||
Extrude {
|
||||
depth: num(p, "depth", 50.0),
|
||||
cap: flag(p, "cap", true),
|
||||
bevel: BevelShape::parse(text(p, "bevel", "none")).map(|shape| Bevel {
|
||||
shape,
|
||||
width: height,
|
||||
height,
|
||||
inside: flag(p, "bevelInside", false),
|
||||
..Bevel::default()
|
||||
}),
|
||||
hidden: flag(p, "drawHiddenFaces", false) || classic_shading(p).surface == ClassicSurface::Wireframe,
|
||||
..Extrude::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The color faces darken toward: black, the custom color, or none (faces keep their paint and only
|
||||
/// the highlight lightens them).
|
||||
fn shading_color(p: &Value) -> Option<Color> {
|
||||
match text(p, "shadingColor", "black") {
|
||||
"none" => None,
|
||||
"custom" => Some(p.get("shadingCustom").and_then(Value::as_str).and_then(parse_hex).unwrap_or(Color::BLACK)),
|
||||
_ => Some(Color::BLACK),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_hex(s: &str) -> Option<Color> {
|
||||
(s.len() == 7 && s.starts_with('#') && s.bytes().skip(1).all(|b| b.is_ascii_hexdigit())).then(|| Color::from_hex(s)).flatten()
|
||||
}
|
||||
|
||||
/// The lights: the `lights` list when it is given, else the one light of `lightRotation`,
|
||||
/// `lightHeight`, `lightBehind` and `lightIntensity`. Each is its direction toward the light in view
|
||||
/// space and its intensity (0..1).
|
||||
fn lights(p: &Value) -> Vec<([f64; 3], f64)> {
|
||||
if let Some(list) = crate::lighting::lights(p) {
|
||||
return list.iter().map(|l| (l.direction(), l.intensity)).collect();
|
||||
}
|
||||
let light = Light {
|
||||
rotation: num(p, "lightRotation", vectorcraft_three_d::DEFAULT_LIGHT.0),
|
||||
height: num(p, "lightHeight", vectorcraft_three_d::DEFAULT_LIGHT.1),
|
||||
behind: flag(p, "lightBehind", false),
|
||||
..Light::default()
|
||||
};
|
||||
vec![(light.direction(), num(p, "lightIntensity", 100.0) / 100.0)]
|
||||
}
|
||||
|
||||
/// The numeric parameters: (key, lowest, highest, whole numbers only).
|
||||
const RANGES: [(&str, f64, f64, bool); 13] = [
|
||||
("depth", 0.0, 2000.0, false),
|
||||
("bevelHeight", 1.0, 100.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; 4] = ["cap", "bevelInside", "lightBehind", "drawHiddenFaces"];
|
||||
|
||||
/// Check Extrude & Bevel (Classic)'s parameters `p` (its defaults filled in): every key is one of
|
||||
/// 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> {
|
||||
let Some(map) = p.as_object() else { return Ok(()) };
|
||||
let defaults = crate::default_params(CLASSIC_EXTRUDE).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()) {
|
||||
let keys = known.join(", ");
|
||||
return Err(format!("unknown parameter `{key}`; the command takes: {keys}"));
|
||||
}
|
||||
}
|
||||
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)) {
|
||||
return Err(format!("`{key}` must be a whole number between {min} and {max} (got {x})"));
|
||||
}
|
||||
if !(min..=max).contains(&x) {
|
||||
return Err(format!("`{key}` must be between {min} and {max} (got {x})"));
|
||||
}
|
||||
}
|
||||
for key in FLAGS {
|
||||
if map.get(key).is_some_and(|v| !v.is_boolean()) {
|
||||
return Err(format!("`{key}` must be true or false"));
|
||||
}
|
||||
}
|
||||
let one_of = |key: &str, ids: &[&str]| -> Result<(), String> {
|
||||
match map.get(key) {
|
||||
Some(v) if !v.as_str().is_some_and(|s| ids.contains(&s)) => Err(format!("`{key}` must be one of {}", ids.join(", "))),
|
||||
_ => Ok(()),
|
||||
}
|
||||
};
|
||||
let surfaces: Vec<&str> = ClassicSurface::ALL.iter().map(|s| s.id()).collect();
|
||||
one_of("surface", &surfaces)?;
|
||||
one_of("shadingColor", &SHADING_COLORS)?;
|
||||
let key = "shadingCustom";
|
||||
if map.get(key).is_some_and(|v| !v.as_str().is_some_and(|s| parse_hex(s).is_some())) {
|
||||
return Err(format!("`{key}` must be a #rrggbb color"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `c` with tone `tone` (0 the shading color, 1 the color itself) and highlight `spec` (toward
|
||||
/// white) on top.
|
||||
fn shaded(c: Color, tone: f64, spec: f64, shade: Option<Color>) -> Color {
|
||||
let base = c.to_rgb();
|
||||
let dark = shade.map_or(base, |s| s.to_rgb());
|
||||
let (tone, spec) = (tone as f32, spec as f32);
|
||||
let [r, g, b] = [0, 1, 2].map(|i| {
|
||||
let mixed = dark[i] + (base[i] - dark[i]) * tone;
|
||||
(mixed + (1.0 - mixed) * spec).clamp(0.0, 1.0)
|
||||
});
|
||||
Color::rgb(r, g, b)
|
||||
}
|
||||
|
||||
/// One solid's faces in paint `paint`, as a group with the paint's opacity and blend mode, one child
|
||||
/// 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 {
|
||||
let shading = classic_shading(p);
|
||||
let lights = lights(p);
|
||||
let shade = shading_color(p);
|
||||
let wire = shading.surface == ClassicSurface::Wireframe;
|
||||
let (paint, mask) = if wire { (source.clone(), None) } else { crate::lighting::opaque_paint(source) };
|
||||
let paint = &paint;
|
||||
let children = facets
|
||||
.into_iter()
|
||||
.map(|face| {
|
||||
if wire {
|
||||
let mut edge = StrokeLayer::new(paint.clone(), WIRE);
|
||||
edge.join = vectorcraft_doc::LineJoin::Round;
|
||||
return Arc::new(Node::path(n.id, face.path(), Appearance { items: vec![AppearanceItem::Stroke(edge)], ..Default::default() }));
|
||||
}
|
||||
let (tone, spec) = shading.tone(face.normal, &lights);
|
||||
// A narrow stroke of the face's own paint closes the hairline seams between faces.
|
||||
let seam = (face.thickness() / 2.0).clamp(0.05, 0.75);
|
||||
let items = match paint.color() {
|
||||
// Unshaded (No Shading, no shading color, or a face in full light) with no
|
||||
// highlight: the paint itself, its color space and swatch kept.
|
||||
Some(_) if (tone >= 1.0 || shade.is_none()) && spec <= 0.0 => {
|
||||
vec![AppearanceItem::Fill(FillLayer::new(paint.clone())), AppearanceItem::Stroke(StrokeLayer::new(paint.clone(), seam))]
|
||||
}
|
||||
Some(c) => {
|
||||
let fill = Paint::solid(shaded(c, tone, spec, shade));
|
||||
vec![AppearanceItem::Fill(FillLayer::new(fill.clone())), AppearanceItem::Stroke(StrokeLayer::new(fill, seam))]
|
||||
}
|
||||
// Gradients and patterns keep their paint, darkened by a layer of the shading color
|
||||
// and lightened by one of white. As with the other 3D effects' pattern shadow, the
|
||||
// layers cover the whole face, transparent pattern cells included.
|
||||
None => {
|
||||
let mut items =
|
||||
vec![AppearanceItem::Fill(FillLayer::new(paint.clone())), AppearanceItem::Stroke(StrokeLayer::new(paint.clone(), seam))];
|
||||
for (color, share) in [(shade, 1.0 - tone), (Some(Color::WHITE), spec)] {
|
||||
if let Some(color) = color.filter(|_| share > 0.0) {
|
||||
let mut layer = FillLayer::new(Paint::solid(color));
|
||||
layer.opacity = share as f32;
|
||||
items.push(AppearanceItem::Fill(layer));
|
||||
}
|
||||
}
|
||||
items
|
||||
}
|
||||
};
|
||||
Arc::new(Node::path(n.id, face.path(), Appearance { items, ..Default::default() }))
|
||||
})
|
||||
.collect();
|
||||
let mut group = Node::group(n.id, children);
|
||||
group.opacity = opacity;
|
||||
group.blend = blend;
|
||||
if let Some(mask) = mask {
|
||||
let bounds = group.visual_bounds().unwrap_or(Rect::ZERO).inflate(1.0, 1.0);
|
||||
let coverage = Node::path(n.id, vectorcraft_geom::shapes::rectangle(bounds), Appearance::basic(mask, Paint::None, 0.0));
|
||||
group.extras_mut().mask = Some(Box::new(vectorcraft_doc::OpacityMask::new(coverage, true)));
|
||||
}
|
||||
group
|
||||
}
|
||||
@@ -45,10 +45,13 @@ fn every_solid_draws_shaded_faces_and_leaves_the_source_editable() {
|
||||
assert_eq!(n.path_data(), Some(&source), "{id}");
|
||||
assert!(!faces(&art).is_empty(), "{id}");
|
||||
// The faces are the fill's red or the stroke's black, shaded: never brighter, never another
|
||||
// hue.
|
||||
// hue (the classic Plastic surface adds a white highlight, tested on its own).
|
||||
for f in faces(&art) {
|
||||
let [r, g, b] = fill_rgb(f);
|
||||
assert!(r <= 0.8 + 1e-6 && (g * 4.0 - r).abs() < 1e-4 && (b * 8.0 - r).abs() < 1e-4 || id == ROTATE, "{id}: {r} {g} {b}");
|
||||
assert!(
|
||||
r <= 0.8 + 1e-6 && (g * 4.0 - r).abs() < 1e-4 && (b * 8.0 - r).abs() < 1e-4 || id == ROTATE || id == CLASSIC_EXTRUDE,
|
||||
"{id}: {r} {g} {b}"
|
||||
);
|
||||
}
|
||||
// The art follows the source when it moves.
|
||||
let before = art.visual_bounds().unwrap();
|
||||
@@ -595,3 +598,160 @@ fn a_3d_effect_on_a_group_turns_every_member_about_one_shared_axis() {
|
||||
assert!(err.contains("clipping groups"), "{err}");
|
||||
assert!(solid_art(&g).is_none(), "drawn as the group it is");
|
||||
}
|
||||
|
||||
/// The red square without its stroke, with Extrude & Bevel (Classic) and `params`.
|
||||
fn classic(params: Value) -> Node {
|
||||
let mut n = square_node();
|
||||
n.appearance.items.retain(|i| matches!(i, AppearanceItem::Fill(_)));
|
||||
n.appearance.effects.push(fx(CLASSIC_EXTRUDE, params));
|
||||
n
|
||||
}
|
||||
|
||||
fn classic_faces(params: Value) -> Vec<Node> {
|
||||
let art = solid_art(&classic(params)).unwrap();
|
||||
faces(&art).into_iter().cloned().collect()
|
||||
}
|
||||
|
||||
fn close3(a: [f32; 3], b: [f32; 3]) -> bool {
|
||||
a.iter().zip(b).all(|(a, b)| (a - b).abs() < 1e-4)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classic_surfaces_draw_edges_flat_paint_soft_light_and_a_highlight() {
|
||||
// Wireframe: the edges of every face, the hidden ones too, in the paint; no fills.
|
||||
let wire = classic_faces(json!({"surface": "wireframe"}));
|
||||
let plastic = classic_faces(json!({}));
|
||||
assert_eq!(plastic.len(), 3, "a box shows three faces");
|
||||
assert_eq!(wire.len(), 6, "and has six");
|
||||
for f in &wire {
|
||||
let [AppearanceItem::Stroke(s)] = &f.appearance.items[..] else { panic!("a stroke alone: {:?}", f.appearance.items) };
|
||||
assert_eq!(s.paint, Paint::solid(RED));
|
||||
}
|
||||
// No Shading: every face in the art's own paint, a CMYK color kept as it is.
|
||||
assert!(classic_faces(json!({"surface": "none"})).iter().all(|f| close3(fill_rgb(f), RED.to_rgb())));
|
||||
let cmyk = Paint::solid(Color::Cmyk { c: 0.1, m: 0.9, y: 0.8, k: 0.0 });
|
||||
let mut n = classic(json!({"surface": "none"}));
|
||||
if let Some(AppearanceItem::Fill(f)) = n.appearance.items.first_mut() {
|
||||
f.paint = cmyk.clone();
|
||||
}
|
||||
for f in faces(&solid_art(&n).unwrap()) {
|
||||
let AppearanceItem::Fill(fill) = &f.appearance.items[0] else { panic!("a fill first") };
|
||||
assert_eq!(fill.paint, cmyk);
|
||||
}
|
||||
// Diffuse: the red darkened toward black by how far each face turns from the light, so the
|
||||
// faces differ but keep their hue.
|
||||
let diffuse: Vec<[f32; 3]> = classic_faces(json!({"surface": "diffuse"})).iter().map(fill_rgb).collect();
|
||||
assert!(diffuse.iter().all(|[r, g, b]| *r <= 0.8 + 1e-6 && (g * 4.0 - r).abs() < 1e-4 && (b * 8.0 - r).abs() < 1e-4), "{diffuse:?}");
|
||||
assert!(diffuse.windows(2).any(|w| !close3(w[0], w[1])), "{diffuse:?}");
|
||||
// Plastic: the same faces, one of them lightened toward white by the highlight.
|
||||
let shiny: Vec<[f32; 3]> =
|
||||
classic_faces(json!({"highlightIntensity": 100, "highlightSize": 100, "lightHeight": 60})).iter().map(fill_rgb).collect();
|
||||
let soft: Vec<[f32; 3]> = classic_faces(json!({"surface": "diffuse", "lightHeight": 60})).iter().map(fill_rgb).collect();
|
||||
assert!(shiny.iter().zip(&soft).any(|(s, d)| s[1] > d[1] + 0.01), "{shiny:?} {soft:?}");
|
||||
assert!(shiny.iter().zip(&soft).all(|(s, d)| s[1] >= d[1] - 1e-4));
|
||||
// Without highlight, Plastic shades as Diffuse.
|
||||
let dull: Vec<[f32; 3]> = classic_faces(json!({"highlightIntensity": 0, "lightHeight": 60})).iter().map(fill_rgb).collect();
|
||||
assert!(dull.iter().zip(&soft).all(|(a, b)| close3(*a, *b)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classic_shading_color_blend_steps_and_lights() {
|
||||
let base = json!({"surface": "diffuse", "ambient": 20});
|
||||
let with = |extra: Value| {
|
||||
let mut p = base.clone();
|
||||
p.as_object_mut().unwrap().extend(extra.as_object().unwrap().clone());
|
||||
classic_faces(p).iter().map(fill_rgb).collect::<Vec<_>>()
|
||||
};
|
||||
// A custom shading color darkens toward that color; None keeps the paint.
|
||||
assert!(with(json!({"shadingColor": "custom", "shadingCustom": "#0000ff"})).iter().any(|c| c[2] > c[0] / 8.0 + 0.01));
|
||||
assert!(with(json!({"shadingColor": "none"})).iter().all(|c| close3(*c, RED.to_rgb())));
|
||||
// One blend step: every face fully lit or fully in shadow.
|
||||
let stepped = with(json!({"blendSteps": 1, "ambient": 0}));
|
||||
assert!(stepped.iter().all(|c| close3(*c, RED.to_rgb()) || close3(*c, [0.0; 3])), "{stepped:?}");
|
||||
// No light: the ambient share of the red. Two lights add up.
|
||||
let dark = with(json!({"lightIntensity": 0, "blendSteps": 256}));
|
||||
assert!(dark.iter().all(|c| (c[0] - 0.8 * 0.2).abs() < 0.01), "{dark:?}");
|
||||
let one = with(json!({"lights": [{"intensity": 30}], "blendSteps": 256}));
|
||||
let two = with(json!({"lights": [{"intensity": 30}, {"intensity": 30}], "blendSteps": 256}));
|
||||
assert!(one.iter().zip(&two).all(|(a, b)| b[0] >= a[0] - 1e-4) && one.iter().zip(&two).any(|(a, b)| b[0] > a[0] + 0.01));
|
||||
// Draw Hidden Faces keeps the faces turned away, drawn first.
|
||||
assert_eq!(with(json!({"drawHiddenFaces": true})).len(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classic_bevel_height_extent_and_cap_shape_the_solid() {
|
||||
let bounds = |p: Value| solid_art(&classic(p)).unwrap().visual_bounds().unwrap();
|
||||
let front = json!({"rotationX": 0, "rotationY": 0, "rotationZ": 0, "bevel": "classic", "bevelHeight": 10});
|
||||
let out = bounds(front.clone());
|
||||
let mut inside = front.clone();
|
||||
inside["bevelInside"] = json!(true);
|
||||
// Straight on, a bevel added around the shape widens it; one carved into it keeps its size.
|
||||
assert!(out.width() > 100.0 + 15.0, "{out:?}");
|
||||
let carved = bounds(inside);
|
||||
assert!((carved.width() - 100.0).abs() < 2.0, "{carved:?}");
|
||||
// Hollow: no caps, so the walls seen from inside show too.
|
||||
assert!(classic_faces(json!({"cap": false})).len() > classic_faces(json!({})).len());
|
||||
// The depth sets how far back the solid goes.
|
||||
let side = json!({"rotationX": 0, "rotationY": 90, "rotationZ": 0});
|
||||
let mut deep = side.clone();
|
||||
deep["depth"] = json!(120);
|
||||
assert!((bounds(deep).width() - 120.0).abs() < 1.0);
|
||||
}
|
||||
|
||||
/// A gradient fill with a transparent stop is shaded opaque and masked by its own transparency, so
|
||||
/// the shading does not cover the transparent part; an opaque gradient needs no mask.
|
||||
#[test]
|
||||
fn classic_shading_keeps_a_gradients_transparency() {
|
||||
use vectorcraft_doc::color::{Gradient, GradientKind, GradientPaint, GradientStop};
|
||||
for (end, masked) in [(0.25, true), (1.0, false)] {
|
||||
let mut stop = GradientStop::new(1.0, RED);
|
||||
stop.opacity = end;
|
||||
let gradient = GradientPaint::new(Gradient::new(GradientKind::Linear, vec![GradientStop::new(0.0, RED), stop]));
|
||||
let mut n = classic(json!({}));
|
||||
if let Some(AppearanceItem::Fill(f)) = n.appearance.items.first_mut() {
|
||||
f.paint = Paint::Gradient(Box::new(gradient));
|
||||
}
|
||||
let art = solid_art(&n).unwrap();
|
||||
let surface = &art.children().unwrap()[0];
|
||||
assert_eq!(surface.extras().mask.is_some(), masked, "{end}");
|
||||
let mut opaque = true;
|
||||
surface.walk(&mut |c| {
|
||||
for item in &c.appearance.items {
|
||||
if let AppearanceItem::Fill(f) = item
|
||||
&& let Paint::Gradient(g) = &f.paint
|
||||
{
|
||||
opaque &= g.gradient.stops.iter().all(|s| s.opacity == 1.0);
|
||||
}
|
||||
}
|
||||
});
|
||||
assert!(opaque, "{end}: the faces are drawn opaque");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classic_parameters_are_checked() {
|
||||
let n = classic(json!({}));
|
||||
let check = |p: Value| validate_solid(&n, CLASSIC_EXTRUDE, &crate::merged_params(CLASSIC_EXTRUDE, &p));
|
||||
assert!(check(json!({})).is_ok());
|
||||
assert!(check(json!({"surface": "wireframe", "shadingColor": "custom", "shadingCustom": "#12aBcd", "blendSteps": 256, "bevel": "ogee"})).is_ok());
|
||||
for (p, says) in [
|
||||
(json!({"surface": "glossy"}), "`surface` must be one of wireframe, none, diffuse, plastic"),
|
||||
(json!({"shadingColor": "red"}), "`shadingColor` must be one of black, custom, none"),
|
||||
(json!({"shadingCustom": "blue"}), "`shadingCustom` must be a #rrggbb color"),
|
||||
(json!({"shadingCustom": "#fff"}), "`shadingCustom` must be a #rrggbb color"),
|
||||
(json!({"depth": 2001}), "`depth` must be between 0 and 2000 (got 2001)"),
|
||||
(json!({"bevelHeight": 0}), "`bevelHeight` must be between 1 and 100 (got 0)"),
|
||||
(json!({"ambient": -1}), "`ambient` must be between 0 and 100 (got -1)"),
|
||||
(json!({"blendSteps": 2.5}), "`blendSteps` must be a whole number between 1 and 256 (got 2.5)"),
|
||||
(json!({"blendSteps": 0}), "`blendSteps` must be a whole number between 1 and 256 (got 0)"),
|
||||
(json!({"highlightSize": "big"}), "`highlightSize` must be a number"),
|
||||
(json!({"cap": "yes"}), "`cap` must be true or false"),
|
||||
(json!({"drawHiddenFaces": 1}), "`drawHiddenFaces` must be true or false"),
|
||||
(json!({"bevel": "fancy"}), "bevel must be none or one of"),
|
||||
(json!({"twist": 10}), "unknown parameter `twist`"),
|
||||
(json!({"lights": []}), "lights must contain one to ten lights"),
|
||||
] {
|
||||
let err = check(p.clone()).unwrap_err();
|
||||
assert!(err.starts_with(says), "{p}: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ mod visibility;
|
||||
pub use solid::{Bevel, BevelShape, Extrude, Facet, Inflate, View, extrude, inflate, inflate_together, plane};
|
||||
pub use visibility::{VisibleFace, visible_faces};
|
||||
mod shading;
|
||||
pub use shading::{LightGradient, RgbGradient, ShadeGradient};
|
||||
pub use shading::{ClassicShading, ClassicSurface, LightGradient, RgbGradient, ShadeGradient};
|
||||
mod lights;
|
||||
pub use lights::{Light, Lights, MAX_LIGHTS};
|
||||
mod caps;
|
||||
|
||||
@@ -1,7 +1,99 @@
|
||||
//! Gouraud lighting represented by an ordinary vector linear gradient on each triangle.
|
||||
//! Gouraud lighting represented by an ordinary vector linear gradient on each triangle, and the
|
||||
//! flat surface shading of the classic 3D effects.
|
||||
|
||||
use vectorcraft_geom::{Point, Vec2};
|
||||
|
||||
/// The surface of a classic 3D effect: how its faces are drawn and lit.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum ClassicSurface {
|
||||
/// Only the outline of every face, hidden ones included; the faces are transparent.
|
||||
Wireframe,
|
||||
/// Every face in the art's own paint, unlit.
|
||||
NoShading,
|
||||
/// Soft light: each face as bright as it faces the light.
|
||||
Diffuse,
|
||||
/// Diffuse light plus a glossy highlight where a face mirrors the light toward the viewer.
|
||||
#[default]
|
||||
Plastic,
|
||||
}
|
||||
|
||||
impl ClassicSurface {
|
||||
pub const ALL: [ClassicSurface; 4] = [Self::Wireframe, Self::NoShading, Self::Diffuse, Self::Plastic];
|
||||
|
||||
pub fn id(self) -> &'static str {
|
||||
match self {
|
||||
Self::Wireframe => "wireframe",
|
||||
Self::NoShading => "none",
|
||||
Self::Diffuse => "diffuse",
|
||||
Self::Plastic => "plastic",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(s: &str) -> Option<Self> {
|
||||
Self::ALL.into_iter().find(|v| v.id() == s)
|
||||
}
|
||||
}
|
||||
|
||||
/// The surface settings of a classic 3D effect. Shares are 0..1.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct ClassicShading {
|
||||
pub surface: ClassicSurface,
|
||||
/// Light every face gets, whichever way it faces.
|
||||
pub ambient: f64,
|
||||
/// How bright the highlight is at its center (Plastic only).
|
||||
pub highlight_intensity: f64,
|
||||
/// How wide the highlight spreads: 1 broad, 0 a pinpoint (Plastic only).
|
||||
pub highlight_size: f64,
|
||||
/// How many levels the shading and the highlight are rounded to, 1..256.
|
||||
pub blend_steps: u32,
|
||||
}
|
||||
|
||||
impl Default for ClassicShading {
|
||||
fn default() -> Self {
|
||||
Self { surface: ClassicSurface::Plastic, ambient: 0.5, highlight_intensity: 0.6, highlight_size: 0.9, blend_steps: 25 }
|
||||
}
|
||||
}
|
||||
|
||||
impl ClassicShading {
|
||||
/// The shine exponent of the highlight: 2 at the largest size, 256 at the smallest.
|
||||
fn shininess(self) -> f64 {
|
||||
2f64.powf(1.0 + 7.0 * (1.0 - self.highlight_size.clamp(0.0, 1.0)))
|
||||
}
|
||||
|
||||
/// `x` (0..1) rounded to the nearest of the blend steps' levels.
|
||||
fn stepped(self, x: f64) -> f64 {
|
||||
let steps = f64::from(self.blend_steps.clamp(1, 256));
|
||||
((x.clamp(0.0, 1.0) * steps).round() / steps).clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
/// The tone (0 in full shadow, 1 fully lit) and the highlight (0 none, 1 white) of a face with
|
||||
/// view-space unit normal `normal`, seen from the front and lit by `lights`: each a unit
|
||||
/// direction toward the light and its intensity (0..1). The lights add up. Wireframe and No
|
||||
/// Shading are unlit: tone 1, no highlight.
|
||||
pub fn tone(self, normal: [f64; 3], lights: &[([f64; 3], f64)]) -> (f64, f64) {
|
||||
if matches!(self.surface, ClassicSurface::Wireframe | ClassicSurface::NoShading) {
|
||||
return (1.0, 0.0);
|
||||
}
|
||||
let dot = |a: [f64; 3], b: [f64; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
||||
let (mut tone, mut spec) = (self.ambient.clamp(0.0, 1.0), 0.0);
|
||||
for &(light, intensity) in lights {
|
||||
let intensity = if intensity.is_finite() { intensity.clamp(0.0, 1.0) } else { 0.0 };
|
||||
let lit = dot(normal, light).max(0.0);
|
||||
tone += intensity * lit;
|
||||
// The halfway direction between the light and the eye in front of the page.
|
||||
let half = [light[0], light[1], light[2] + 1.0];
|
||||
let length = dot(half, half).sqrt();
|
||||
if self.surface == ClassicSurface::Plastic && length > 1e-12 && lit > 0.0 {
|
||||
spec += intensity * (dot(normal, half) / length).max(0.0).powf(self.shininess()) * self.highlight_intensity.clamp(0.0, 1.0);
|
||||
}
|
||||
}
|
||||
if !tone.is_finite() || !spec.is_finite() {
|
||||
return (1.0, 0.0);
|
||||
}
|
||||
(self.stepped(tone), self.stepped(spec))
|
||||
}
|
||||
}
|
||||
|
||||
/// Document-space gradient placement and the brightness at its two ends. Clipping a
|
||||
/// face must keep this placement unchanged, so newly cut edges retain their lighting.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
|
||||
@@ -201,11 +201,14 @@ pub struct Extrude {
|
||||
/// Closed ends (a solid) or open ones (a hollow tube).
|
||||
pub cap: bool,
|
||||
pub bevel: Option<Bevel>,
|
||||
/// Keep the faces of a closed solid that are turned away from the viewer (Draw Hidden Faces,
|
||||
/// and the classic Wireframe surface), drawn behind the faces turned toward it.
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
||||
impl Default for Extrude {
|
||||
fn default() -> Self {
|
||||
Self { depth: 50.0, twist: 0.0, taper: 1.0, cap: true, bevel: None }
|
||||
Self { depth: 50.0, twist: 0.0, taper: 1.0, cap: true, bevel: None, hidden: false }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +242,9 @@ pub struct Facet {
|
||||
pub depth: f64,
|
||||
pub brightness: f64,
|
||||
pub illumination: [f64; 3],
|
||||
/// 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],
|
||||
/// Smooth lighting on an inflated triangle, before clipping individual RGB channels.
|
||||
pub rgb_gradient: Option<crate::RgbGradient>,
|
||||
/// The contours in view space, before projection, for tests that measure depths.
|
||||
|
||||
@@ -47,6 +47,9 @@ pub(super) struct Builder<'a> {
|
||||
/// A closed solid hides the faces turned away from the viewer; an open one shows their
|
||||
/// insides.
|
||||
pub(super) closed: bool,
|
||||
/// A closed solid keeps the faces turned away from the viewer too, with their outward normals,
|
||||
/// drawn behind the others.
|
||||
pub(super) hidden: bool,
|
||||
pub(super) facets: Vec<Facet>,
|
||||
}
|
||||
|
||||
@@ -83,7 +86,7 @@ impl Builder<'_> {
|
||||
// leave gaps.
|
||||
let (n, behind) = if facing > 0.0 {
|
||||
(n, false)
|
||||
} else if self.closed && facing > -0.02 {
|
||||
} else if self.closed && (facing > -0.02 || self.hidden) {
|
||||
(n, true)
|
||||
} else if self.closed {
|
||||
return Ok(());
|
||||
@@ -111,6 +114,7 @@ impl Builder<'_> {
|
||||
depth,
|
||||
brightness: self.camera.brightness(n),
|
||||
illumination: crate::illumination(&self.camera.view, n, self.camera.light),
|
||||
normal: [n.0, n.1, n.2],
|
||||
rgb_gradient: None,
|
||||
#[cfg(test)]
|
||||
view,
|
||||
@@ -378,7 +382,7 @@ pub fn extrude(path: &PathData, rule: FillRule, bounds: Rect, options: Extrude,
|
||||
}
|
||||
let rings = rings(path, rule, bounds.width().max(bounds.height()), MAX_SOLID_POINTS)?;
|
||||
let camera = Camera::new(view, bounds, o.depth);
|
||||
let mut b = Builder { camera: &camera, closed: o.cap, facets: vec![] };
|
||||
let mut b = Builder { camera: &camera, closed: o.cap, hidden: o.hidden, facets: vec![] };
|
||||
let steps = steps(&o);
|
||||
let z = |t: f64| o.depth / 2.0 - o.depth * t;
|
||||
let ring_at =
|
||||
@@ -431,7 +435,7 @@ pub fn plane(path: &PathData, rule: FillRule, bounds: Rect, view: View) -> Resul
|
||||
check_bounds(bounds)?;
|
||||
let rings = rings(path, rule, bounds.width().max(bounds.height()), MAX_PLANE_POINTS)?;
|
||||
let camera = Camera::new(view, bounds, 0.0);
|
||||
let mut b = Builder { camera: &camera, closed: false, facets: vec![] };
|
||||
let mut b = Builder { camera: &camera, closed: false, hidden: false, facets: vec![] };
|
||||
let contours: Vec<Vec<V3>> = rings.iter().map(|r| r.iter().map(|p| camera.place(*p, 0.0)).collect()).collect();
|
||||
// A plane is one flat face, seen from either side. Its normal is turned toward the viewer, so
|
||||
// it is drawn last, as a cap is, and planes drawn in one order keep the order they are given in.
|
||||
|
||||
@@ -106,7 +106,7 @@ fn inflated(p: Puff, bounds: Rect, o: Inflate, view: View, reach: f64) -> Result
|
||||
let smooth = view.shade && view.has_colored_lights();
|
||||
let camera = Camera::new(view, bounds, o.depth + 2.0 * height.max(reach));
|
||||
// Without its back cap the object is a shell open behind, its inside showing.
|
||||
let mut b = Builder { camera: &camera, closed: o.cap, facets: vec![] };
|
||||
let mut b = Builder { camera: &camera, closed: o.cap, hidden: false, facets: vec![] };
|
||||
let half = o.depth / 2.0;
|
||||
// The back: turned and scaled by the twist and taper, its puff too.
|
||||
let back_turn = Turn::new(bounds, o.twist, o.taper, 1.0);
|
||||
|
||||
@@ -76,6 +76,28 @@ fn no_depth_is_the_flat_shape_and_no_cap_shows_the_inside() {
|
||||
assert!(hollow.iter().all(|f| f.contours.len() == 1 && f.contours[0].len() == 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_faces_draw_every_face_of_the_solid_behind_the_visible_ones() {
|
||||
let (p, b) = shape(&square(0.0, 0.0, 100.0));
|
||||
let v = View { rotation_x: 20.0, rotation_y: 30.0, ..front() };
|
||||
let shown = extrude(&p, FillRule::EvenOdd, b, Extrude::default(), v).unwrap();
|
||||
let all = extrude(&p, FillRule::EvenOdd, b, Extrude { hidden: true, ..Extrude::default() }, v).unwrap();
|
||||
// A box: the three faces turned to the viewer, then the three turned away, drawn first.
|
||||
assert_eq!(shown.len(), 3);
|
||||
assert_eq!(all.len(), 6);
|
||||
assert!(all.windows(2).all(|w| w[0].depth <= w[1].depth));
|
||||
// The faces turned away keep their outward normals, so a light behind the solid lights them.
|
||||
assert!(all[..3].iter().all(|f| f.normal[2] < 0.0), "{:?}", all.iter().map(|f| f.normal).collect::<Vec<_>>());
|
||||
for f in shown.iter().chain(&all) {
|
||||
let [x, y, z] = f.normal;
|
||||
assert!(close(x.hypot(y).hypot(z), 1.0, 1e-9), "{:?}", f.normal);
|
||||
}
|
||||
// The visible faces keep their normals and their place at the front.
|
||||
for (front, kept) in shown.iter().rev().zip(all.iter().rev()) {
|
||||
assert_eq!(front.normal, kept.normal);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn taper_shrinks_the_back_and_twist_turns_it() {
|
||||
let (p, b) = shape(&square(0.0, 0.0, 100.0));
|
||||
@@ -232,6 +254,7 @@ fn malformed_and_degenerate_input_is_bounded() {
|
||||
taper: -3.0,
|
||||
cap: true,
|
||||
bevel: Some(Bevel { width: f64::NAN, height: 1e12, repeat: usize::MAX, space: 7.0, ..Bevel::default() }),
|
||||
hidden: false,
|
||||
};
|
||||
let f = extrude(&p, FillRule::EvenOdd, b, wild, View { rotation_x: f64::NAN, perspective: 900.0, ..front() }).unwrap();
|
||||
assert!(f.iter().flat_map(|f| f.contours.iter().flatten()).all(|q| q.x.is_finite() && q.y.is_finite()));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::*;
|
||||
|
||||
mod caps;
|
||||
mod classic;
|
||||
mod lighting;
|
||||
mod multiple_lights;
|
||||
mod smooth;
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
use crate::*;
|
||||
|
||||
const TOWARD: [f64; 3] = [0.0, 0.0, 1.0];
|
||||
|
||||
fn close(a: f64, b: f64) -> bool {
|
||||
(a - b).abs() < 1e-9
|
||||
}
|
||||
|
||||
fn one(light: [f64; 3], intensity: f64) -> [([f64; 3], f64); 1] {
|
||||
[(light, intensity)]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wireframe_and_no_shading_leave_every_face_unlit() {
|
||||
for surface in [ClassicSurface::Wireframe, ClassicSurface::NoShading] {
|
||||
let s = ClassicShading { surface, ..ClassicShading::default() };
|
||||
for normal in [TOWARD, [1.0, 0.0, 0.0], [0.0, 0.0, -1.0]] {
|
||||
assert_eq!(s.tone(normal, &one(TOWARD, 1.0)), (1.0, 0.0), "{surface:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diffuse_adds_the_light_to_the_ambient_and_has_no_highlight() {
|
||||
let s = ClassicShading { surface: ClassicSurface::Diffuse, ambient: 0.3, blend_steps: 256, ..ClassicShading::default() };
|
||||
let (lit, spec) = s.tone(TOWARD, &one(TOWARD, 0.4));
|
||||
assert!((lit - 0.7).abs() < 1.0 / 256.0, "{lit}");
|
||||
assert_eq!(spec, 0.0);
|
||||
// A face turned away from the light gets the ambient light only.
|
||||
let (away, _) = s.tone(TOWARD, &one([1.0, 0.0, 0.0], 0.4));
|
||||
assert!((away - 0.3).abs() < 1.0 / 256.0, "{away}");
|
||||
// A face at 60° to the light gets half of it.
|
||||
let (half, _) = s.tone(TOWARD, &one([3f64.sqrt() / 2.0, 0.0, 0.5], 0.4));
|
||||
assert!((half - 0.5).abs() < 1.0 / 256.0, "{half}");
|
||||
// The tone stays within full shadow and full light.
|
||||
assert_eq!(ClassicShading { ambient: 1.0, ..s }.tone(TOWARD, &one(TOWARD, 1.0)).0, 1.0);
|
||||
// Without lights a face gets the ambient light only.
|
||||
assert!((s.tone(TOWARD, &[]).0 - 0.3).abs() < 1.0 / 256.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lights_add_up() {
|
||||
let s = ClassicShading { surface: ClassicSurface::Diffuse, ambient: 0.0, blend_steps: 256, ..ClassicShading::default() };
|
||||
let left = [-0.6, 0.0, 0.8];
|
||||
let right = [0.6, 0.0, 0.8];
|
||||
let both = s.tone(TOWARD, &[(left, 0.3), (right, 0.3)]).0;
|
||||
let single = s.tone(TOWARD, &one(left, 0.3)).0;
|
||||
assert!((both - 2.0 * single).abs() < 2.0 / 256.0, "{both} {single}");
|
||||
// A light behind the object lights the faces turned away from the viewer only.
|
||||
assert_eq!(s.tone(TOWARD, &one([0.0, 0.0, -1.0], 1.0)).0, 0.0);
|
||||
assert!(s.tone([0.0, 0.0, -1.0], &one([0.0, 0.0, -1.0], 1.0)).0 > 0.99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plastic_highlight_peaks_where_a_face_mirrors_the_light_to_the_eye() {
|
||||
let s = ClassicShading { highlight_intensity: 0.8, blend_steps: 256, ..ClassicShading::default() };
|
||||
let light = one([0.6, 0.0, 0.8], 1.0);
|
||||
// The face halfway between the light and the eye mirrors the light straight to the viewer.
|
||||
let length = 0.6f64.hypot(1.8);
|
||||
let half = [0.6 / length, 0.0, 1.8 / length];
|
||||
let (_, peak) = s.tone(half, &light);
|
||||
assert!((peak - 0.8).abs() < 1.0 / 256.0, "{peak}");
|
||||
let (_, aside) = s.tone([-0.6, 0.0, 0.8], &light);
|
||||
assert!(aside < peak / 4.0, "{aside}");
|
||||
// A smaller highlight falls off faster.
|
||||
let tilted = [0.3, 0.0, 0.954];
|
||||
let broad = s.tone(tilted, &light).1;
|
||||
let narrow = ClassicShading { highlight_size: 0.1, ..s }.tone(tilted, &light).1;
|
||||
assert!(narrow < broad, "{narrow} {broad}");
|
||||
// No highlight on a face the light does not reach, without Highlight Intensity, or on Diffuse.
|
||||
assert_eq!(s.tone(TOWARD, &one([0.0, 0.0, -1.0], 1.0)).1, 0.0);
|
||||
assert_eq!(ClassicShading { highlight_intensity: 0.0, ..s }.tone(half, &light).1, 0.0);
|
||||
assert_eq!(ClassicShading { surface: ClassicSurface::Diffuse, ..s }.tone(half, &light).1, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blend_steps_round_the_shading_to_that_many_levels() {
|
||||
let s = ClassicShading { surface: ClassicSurface::Diffuse, ambient: 0.0, blend_steps: 2, ..ClassicShading::default() };
|
||||
let tones: Vec<f64> = (0..=20).map(|k| f64::from(k) / 20.0).map(|c| s.tone(TOWARD, &one([(1.0 - c * c).sqrt(), 0.0, c], 0.5)).0).collect();
|
||||
assert!(tones.iter().all(|t| close(*t, 0.0) || close(*t, 0.5)), "{tones:?}");
|
||||
assert!(tones.iter().any(|t| close(*t, 0.0)) && tones.iter().any(|t| close(*t, 0.5)), "{tones:?}");
|
||||
// Out-of-range step counts and intensities are kept within their ranges.
|
||||
for steps in [0, 100_000] {
|
||||
let t = ClassicShading { blend_steps: steps, ..s }.tone(TOWARD, &one(TOWARD, f64::NAN)).0;
|
||||
assert!((0.0..=1.0).contains(&t));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surfaces_round_trip_through_their_ids() {
|
||||
for surface in ClassicSurface::ALL {
|
||||
assert_eq!(ClassicSurface::parse(surface.id()), Some(surface));
|
||||
}
|
||||
assert_eq!(ClassicSurface::parse("glossy"), None);
|
||||
assert_eq!(ClassicSurface::default(), ClassicSurface::Plastic);
|
||||
}
|
||||
@@ -120,6 +120,7 @@ pub mod symbolism_options;
|
||||
pub mod tate_chu_yoko;
|
||||
mod text_export;
|
||||
pub mod text_import;
|
||||
mod three_d_classic;
|
||||
mod tiff_bmp_tga;
|
||||
pub mod tile_edge_color;
|
||||
mod tools;
|
||||
|
||||
@@ -346,15 +346,20 @@ 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, Extrude & Bevel's bevel (Rounded translated in the "3d" context
|
||||
/// first), every Warp style's orientation, Rasterize's Color Model (its `document` value labeled
|
||||
/// 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
|
||||
/// 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::EXTRUDE, "bevel") => vec![
|
||||
(vectorcraft_effects::CLASSIC_EXTRUDE, "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::EXTRUDE | vectorcraft_effects::CLASSIC_EXTRUDE, "bevel") => vec![
|
||||
(tl!("None"), "none"),
|
||||
(tl!("Classic"), "classic"),
|
||||
(c3("Rounded"), "rounded"),
|
||||
@@ -915,6 +920,9 @@ mod tests {
|
||||
for (effect, key) in [
|
||||
(vectorcraft_effects::REVOLVE, "edge"),
|
||||
(vectorcraft_effects::EXTRUDE, "bevel"),
|
||||
(vectorcraft_effects::CLASSIC_EXTRUDE, "bevel"),
|
||||
(vectorcraft_effects::CLASSIC_EXTRUDE, "surface"),
|
||||
(vectorcraft_effects::CLASSIC_EXTRUDE, "shadingColor"),
|
||||
("warp.arc", "orientation"),
|
||||
("warp.twist", "orientation"),
|
||||
(vectorcraft_effects::RASTERIZE, "background"),
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
//! Compact controls for the 3D and Materials effects: Extrude & Bevel, Revolve, Inflate and Rotate.
|
||||
//! 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.
|
||||
//! 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
|
||||
//! 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::{EXTRUDE, INFLATE, REVOLVE, ROTATION_PRESETS};
|
||||
use vectorcraft_effects::{CLASSIC_EXTRUDE, EXTRUDE, INFLATE, REVOLVE, ROTATION_PRESETS};
|
||||
|
||||
use crate::state::Dialog;
|
||||
use crate::theme::Tokens;
|
||||
@@ -136,14 +137,15 @@ fn controls(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
}
|
||||
|
||||
/// `s` as the 3D effects mean it (a solid's cap, not a stroke's; a bevelled edge, not a join).
|
||||
fn c3(s: &'static str) -> &'static str {
|
||||
pub(super) fn c3(s: &'static str) -> &'static str {
|
||||
crate::i18n::tr_ctx(crate::i18n::current(), "3d", s)
|
||||
}
|
||||
|
||||
/// Extrude & Bevel and Inflate: 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 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.
|
||||
fn solid_controls(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, id: &str) {
|
||||
let object = id == EXTRUDE || id == INFLATE;
|
||||
let object = id == EXTRUDE || id == INFLATE || id == CLASSIC_EXTRUDE;
|
||||
let mut tabs = vec![];
|
||||
if object {
|
||||
tabs.push(tl!("Object"));
|
||||
@@ -163,10 +165,16 @@ fn solid_controls(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, id: &
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
if super::revolve_gizmo::lighting(d) {
|
||||
lighting_controls(ui, d);
|
||||
if id == CLASSIC_EXTRUDE {
|
||||
super::three_d_classic::surface_controls(ui, d);
|
||||
} else {
|
||||
lighting_controls(ui, d);
|
||||
}
|
||||
} else if object && d.str("__tab") != "rotation" {
|
||||
let unit = app.session.general_unit();
|
||||
if id == EXTRUDE {
|
||||
if id == CLASSIC_EXTRUDE {
|
||||
super::three_d_classic::object_controls(ui, d, unit);
|
||||
} else if id == EXTRUDE {
|
||||
extrude_controls(ui, d, unit);
|
||||
} else {
|
||||
heading(ui, tl!("Object"));
|
||||
@@ -245,7 +253,7 @@ fn view_controls(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
}
|
||||
|
||||
/// A dropdown of `options` (value, label) for `key`, with its reset dot.
|
||||
fn choice(ui: &mut egui::Ui, d: &mut Dialog, key: &str, name: &str, options: &[(Value, &str)]) {
|
||||
pub(super) fn choice(ui: &mut egui::Ui, d: &mut Dialog, key: &str, name: &str, options: &[(Value, &str)]) {
|
||||
let id = d.str("__effect");
|
||||
let default = vectorcraft_effects::default_params(&id).and_then(|p| p.get(key).cloned()).unwrap_or(Value::Null);
|
||||
let value = d.fields.get(key).cloned().unwrap_or_else(|| default.clone());
|
||||
@@ -268,7 +276,7 @@ fn choice(ui: &mut egui::Ui, d: &mut Dialog, key: &str, name: &str, options: &[(
|
||||
}
|
||||
|
||||
/// A checkbox for `key`, with its reset dot.
|
||||
fn toggle(ui: &mut egui::Ui, d: &mut Dialog, key: &str, name: &str, default: bool) {
|
||||
pub(super) fn toggle(ui: &mut egui::Ui, d: &mut Dialog, key: &str, name: &str, default: bool) {
|
||||
ui.horizontal(|ui| {
|
||||
let on = d.bool(key);
|
||||
if widgets::check(ui, name, on, ui.is_enabled()) {
|
||||
@@ -370,11 +378,11 @@ pub(super) fn light_options(d: &Dialog) -> vectorcraft_three_d::Revolve {
|
||||
vectorcraft_effects::revolve_options(&super::form::params(&selected_light(d)))
|
||||
}
|
||||
|
||||
fn light_record(d: &Dialog) -> Value {
|
||||
pub(super) fn light_record(d: &Dialog) -> Value {
|
||||
Value::Object(LIGHT_KEYS.iter().filter_map(|(key, field)| d.fields.get(*field).map(|v| ((*key).into(), v.clone()))).collect())
|
||||
}
|
||||
|
||||
fn store_selected(d: &mut Dialog, selected: &Dialog) {
|
||||
pub(super) fn store_selected(d: &mut Dialog, selected: &Dialog) {
|
||||
if let Some(list) = explicit_lights(d) {
|
||||
let index = light_index(d, list.len());
|
||||
let mut list = list.clone();
|
||||
@@ -397,7 +405,7 @@ pub(super) fn set_light_value(d: &mut Dialog, key: &str, value: Value) {
|
||||
store_selected(d, &selected);
|
||||
}
|
||||
|
||||
fn light_list(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
pub(super) fn light_list(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
let mut list = explicit_lights(d).cloned().unwrap_or_else(|| vec![light_record(d)]);
|
||||
let mut index = light_index(d, list.len());
|
||||
let names: Vec<_> = (1..=list.len()).map(|i| format!("{} {i}", c3("Light"))).collect();
|
||||
@@ -464,12 +472,12 @@ fn light_color(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
});
|
||||
}
|
||||
|
||||
fn heading(ui: &mut egui::Ui, text: &str) {
|
||||
pub(super) fn heading(ui: &mut egui::Ui, text: &str) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
ui.label(egui::RichText::new(text).font(crate::theme::semibold(13.0)).color(t.text));
|
||||
}
|
||||
|
||||
fn label(ui: &mut egui::Ui, name: &str) {
|
||||
pub(super) fn label(ui: &mut egui::Ui, name: &str) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (r, _) = ui.allocate_exact_size(vec2(LABEL_W, 24.0), Sense::hover());
|
||||
ui.put(r, egui::Label::new(egui::RichText::new(name).size(12.5).color(t.text)).halign(egui::Align::Min).truncate()).on_hover_text(name);
|
||||
@@ -482,7 +490,7 @@ fn rail_width(ui: &egui::Ui) -> f32 {
|
||||
|
||||
/// A value can be typed, dragged, scrubbed by its label, or moved on its slider.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn number(ui: &mut egui::Ui, d: &mut Dialog, key: &str, name: &str, range: (f64, f64), suffix: &str, decimals: usize, unit: Option<Unit>) {
|
||||
pub(super) fn number(ui: &mut egui::Ui, d: &mut Dialog, key: &str, name: &str, range: (f64, f64), suffix: &str, decimals: usize, unit: Option<Unit>) {
|
||||
let default = vectorcraft_effects::default_params(&d.str("__effect")).and_then(|p| p.get(key).and_then(Value::as_f64)).unwrap_or(0.0);
|
||||
let from = |n| unit.map_or(n, |u| u.from_pt(n));
|
||||
let to = |n| unit.map_or(n, |u| u.to_pt(n));
|
||||
@@ -570,7 +578,7 @@ fn dot(ui: &egui::Ui, p: Pos2, changed: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_dot(ui: &mut egui::Ui, key: &str, changed: bool, default: &str) -> bool {
|
||||
pub(super) fn reset_dot(ui: &mut egui::Ui, key: &str, changed: bool, default: &str) -> bool {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(24.0, 24.0), Sense::hover());
|
||||
let response = ui.interact(r, Id::unique(("revolve-reset", key)), Sense::click());
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), tl!("Reset to default")));
|
||||
|
||||
@@ -297,6 +297,68 @@ fn the_other_3d_effects_show_their_options_and_position_presets() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extrude & Bevel (Classic): its extrude and bevel options on the Object tab, Position on the
|
||||
/// Rotation tab, and the surface with its lights, highlight, blend steps and shading color on the
|
||||
/// Lighting tab; OK keeps the edit as one undo step.
|
||||
#[test]
|
||||
fn classic_extrude_bevel_shows_its_options_and_applies_as_one_step() {
|
||||
use vectorcraft_effects::CLASSIC_EXTRUDE;
|
||||
let mut h = Harness::with("M100 100 L200 100 L200 200 L100 200 Z", CLASSIC_EXTRUDE, vec2(1400.0, 1000.0));
|
||||
let before = h.app.session.doc().unwrap().doc.clone();
|
||||
h.settle();
|
||||
for text in ["Depth", "Cap", "Bevel Shape", "Height", "Bevel Inside"] {
|
||||
h.rect(text);
|
||||
}
|
||||
assert!(!h.texts.iter().any(|(t, _)| t == "Twist" || t == "Taper" || t == "Width"), "the newer effect's options stay out");
|
||||
assert!(!h.ctx.read_response(Id::unique(("revolve-reset", "bevelHeight"))).unwrap().enabled(), "no bevel: its height is off");
|
||||
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 Intensity",
|
||||
"Highlight Size",
|
||||
"Blend Steps",
|
||||
"Shading Color",
|
||||
"Draw Hidden Faces",
|
||||
] {
|
||||
h.rect(text);
|
||||
}
|
||||
assert!(h.ctx.read_response(Id::unique(("revolve-reset", "highlightSize"))).unwrap().enabled());
|
||||
// The light gizmo is on for a lit surface.
|
||||
assert_eq!(dialogs::revolve_gizmo::hint(h.app.ui.dialog.as_ref().unwrap()), "Drag the light, or its rings. Shift snaps to 15°.");
|
||||
// Diffuse has no highlight; Wireframe and No Shading take no light.
|
||||
h.app.ui.dialog.as_mut().unwrap().fields.insert("surface".into(), json!("diffuse"));
|
||||
h.settle();
|
||||
assert!(!h.ctx.read_response(Id::unique(("revolve-reset", "highlightSize"))).unwrap().enabled());
|
||||
assert!(h.ctx.read_response(Id::unique(("revolve-reset", "ambient"))).unwrap().enabled());
|
||||
h.app.ui.dialog.as_mut().unwrap().fields.insert("surface".into(), json!("wireframe"));
|
||||
h.settle();
|
||||
assert!(!h.ctx.read_response(Id::unique(("revolve-reset", "ambient"))).unwrap().enabled());
|
||||
assert!(h.ctx.read_response(Id::unique(("revolve-reset", "drawHiddenFaces"))).unwrap().enabled());
|
||||
assert_eq!(dialogs::revolve_gizmo::hint(h.app.ui.dialog.as_ref().unwrap()), "Drag values or labels. Double-click a value to type.");
|
||||
// A custom shading color shows its color row.
|
||||
let d = h.app.ui.dialog.as_mut().unwrap();
|
||||
d.fields.insert("surface".into(), json!("diffuse"));
|
||||
d.fields.insert("shadingColor".into(), json!("custom"));
|
||||
h.settle();
|
||||
h.rect("#000000");
|
||||
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_EXTRUDE);
|
||||
assert_eq!((fx.params["surface"].clone(), fx.params["shadingColor"].clone()), (json!("diffuse"), json!("custom")));
|
||||
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));
|
||||
|
||||
@@ -20,8 +20,18 @@ 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
|
||||
/// surface?
|
||||
fn lit(d: &Dialog) -> bool {
|
||||
if d.str("__effect") == vectorcraft_effects::CLASSIC_EXTRUDE {
|
||||
matches!(d.str("surface").as_str(), "diffuse" | "plastic")
|
||||
} else {
|
||||
d.bool("shade")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn hint(d: &Dialog) -> &'static str {
|
||||
if !d.bool("preview") || (lighting(d) && !d.bool("shade")) {
|
||||
if !d.bool("preview") || (lighting(d) && !lit(d)) {
|
||||
tl!("Drag values or labels. Double-click a value to type.")
|
||||
} else if lighting(d) {
|
||||
tl!("Drag the light, or its rings. Shift snaps to 15°.")
|
||||
@@ -190,7 +200,7 @@ fn snap_height(height: f64, snap: bool) -> f64 {
|
||||
|
||||
pub(super) fn show(app: &VectorcraftApp, ctx: &egui::Context, d: &mut Dialog) {
|
||||
let light_mode = lighting(d);
|
||||
if !d.bool("preview") || (light_mode && !d.bool("shade")) {
|
||||
if !d.bool("preview") || (light_mode && !lit(d)) {
|
||||
clear(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
//! 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.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::Unit;
|
||||
use vectorcraft_effects::CLASSIC_EXTRUDE;
|
||||
|
||||
use super::revolve::{c3, choice, heading, label, light_list, light_record, number, reset_dot, selected_light, store_selected, toggle};
|
||||
use crate::state::Dialog;
|
||||
|
||||
/// Depth and cap, then the bevel: its shape, its height and whether it is added around the shape
|
||||
/// or carved into it.
|
||||
pub(super) fn object_controls(ui: &mut egui::Ui, d: &mut Dialog, unit: Unit) {
|
||||
heading(ui, tl!("Object"));
|
||||
number(ui, d, "depth", tl!("Depth"), (0.0, 2000.0), "", 1, Some(unit));
|
||||
choice(ui, d, "cap", c3("Cap"), &[(json!(true), c3("Solid")), (json!(false), c3("Hollow"))]);
|
||||
ui.add_space(10.0);
|
||||
heading(ui, c3("Bevel"));
|
||||
choice(ui, d, "bevel", tl!("Bevel Shape"), &options("bevel"));
|
||||
ui.add_enabled_ui(d.str("bevel") != "none", |ui| {
|
||||
number(ui, d, "bevelHeight", tl!("Height"), (1.0, 100.0), "", 1, Some(unit));
|
||||
toggle(ui, d, "bevelInside", tl!("Bevel Inside"), false);
|
||||
});
|
||||
}
|
||||
|
||||
/// The surface, then for Diffuse and Plastic the lights, Ambient Light, the highlight (Plastic
|
||||
/// only), Blend Steps and Shading Color; Draw Hidden Faces last.
|
||||
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");
|
||||
ui.add_enabled_ui(surface == "diffuse" || surface == "plastic", |ui| {
|
||||
light_list(ui, d);
|
||||
let mut selected = selected_light(d);
|
||||
let before = light_record(&selected);
|
||||
number(ui, &mut selected, "lightIntensity", tl!("Light Intensity"), (0.0, 100.0), "%", 1, None);
|
||||
number(ui, &mut selected, "lightRotation", tl!("Rotation"), (-180.0, 180.0), "°", 1, None);
|
||||
number(ui, &mut selected, "lightHeight", tl!("Height"), (0.0, 90.0), "°", 1, None);
|
||||
toggle(ui, &mut selected, "lightBehind", tl!("Light behind the object"), false);
|
||||
if light_record(&selected) != before {
|
||||
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);
|
||||
});
|
||||
number(ui, d, "blendSteps", tl!("Blend Steps"), (1.0, 256.0), "", 0, None);
|
||||
choice(ui, d, "shadingColor", tl!("Shading Color"), &options("shadingColor"));
|
||||
if d.str("shadingColor") == "custom" {
|
||||
shading_custom(ui, d);
|
||||
}
|
||||
});
|
||||
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()
|
||||
}
|
||||
|
||||
/// The custom shading color: a color button, its hex value and its reset dot.
|
||||
fn shading_custom(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 6.0;
|
||||
label(ui, tl!("Color"));
|
||||
let color = d
|
||||
.fields
|
||||
.get("shadingCustom")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|s| s.len() == 7 && s.starts_with('#'))
|
||||
.and_then(vectorcraft_color::Color::from_hex)
|
||||
.unwrap_or(vectorcraft_color::Color::BLACK);
|
||||
let mut rgb = color.to_rgb_uncalibrated().map(|c| (c * 255.0).round() as u8);
|
||||
if ui.color_edit_button_srgb(&mut rgb).on_hover_text(tl!("Shading Color")).changed() {
|
||||
d.fields.insert("shadingCustom".into(), json!(format!("#{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2])));
|
||||
}
|
||||
ui.label(format!("#{:02X}{:02X}{:02X}", rgb[0], rgb[1], rgb[2]));
|
||||
ui.add_space((ui.available_width() - 24.0).max(0.0));
|
||||
if reset_dot(ui, "shadingCustom", rgb != [0; 3], "#000000") {
|
||||
d.fields.insert("shadingCustom".into(), json!("#000000"));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1079,6 +1079,17 @@ font-technology Variable Променливи
|
||||
Exporting {} Експортиране на {}
|
||||
Export… Експортиране…
|
||||
Extrude & Bevel… Избутване и фасетка…
|
||||
3D (Classic) 3D (класически)
|
||||
Extrude & Bevel (Classic)… Избутване и фасетка (класически)…
|
||||
Surface Повърхност
|
||||
Wireframe Телена рамка
|
||||
No Shading Без засенчване
|
||||
Plastic Пластмаса
|
||||
Highlight Intensity Интензивност на отблясъка
|
||||
Highlight Size Размер на отблясъка
|
||||
Blend Steps Стъпки на преливане
|
||||
Shading Color Цвят на засенчването
|
||||
Draw Hidden Faces Изчертаване на скритите страни
|
||||
Eyedropper Пипета
|
||||
Eyedropper Applies: Пипетата прилага:
|
||||
Eyedropper Options Опции за пипетата
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -449,6 +449,17 @@
|
||||
Vector Effects Vektorski efekti
|
||||
3D and Materials 3D i materijali
|
||||
Extrude & Bevel… Istiskivanje i zakošenje…
|
||||
3D (Classic) 3D (klasično)
|
||||
Extrude & Bevel (Classic)… Istiskivanje i zakošenje (klasično)…
|
||||
Surface Površina
|
||||
Wireframe Žičani model
|
||||
No Shading Bez sjenčenja
|
||||
Plastic Plastika
|
||||
Highlight Intensity Intenzitet odsjaja
|
||||
Highlight Size Veličina odsjaja
|
||||
Blend Steps Koraci prelaza
|
||||
Shading Color Boja sjenčenja
|
||||
Draw Hidden Faces Crtaj skrivene strane
|
||||
Revolve… Rotaciono tijelo…
|
||||
Inflate… Napuhni…
|
||||
Materials… Materijali…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1492,6 +1492,17 @@
|
||||
Exporting {} S’està exportant {}
|
||||
Export… Exporta…
|
||||
Extrude & Bevel… Extrudeix i bisella…
|
||||
3D (Classic) 3D (clàssic)
|
||||
Extrude & Bevel (Classic)… Extrudeix i bisella (clàssic)…
|
||||
Surface Superfície
|
||||
Wireframe Filferro
|
||||
No Shading Sense ombra
|
||||
Plastic Plàstic
|
||||
Highlight Intensity Intensitat de la brillantor
|
||||
Highlight Size Mida de la brillantor
|
||||
Blend Steps Passos de fusió
|
||||
Shading Color Color de l’ombra
|
||||
Draw Hidden Faces Dibuixa les cares ocultes
|
||||
Eyedropper Comptagotes
|
||||
Eyedropper Applies: El comptagotes aplica:
|
||||
Eyedropper Options Opcions del comptagotes
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -457,6 +457,17 @@
|
||||
Vector Effects Vektorové efekty
|
||||
3D and Materials 3D a materiály
|
||||
Extrude & Bevel… Vysunutí a zkosení…
|
||||
3D (Classic) 3D (klasické)
|
||||
Extrude & Bevel (Classic)… Vysunutí a zkosení (klasické)…
|
||||
Surface Povrch
|
||||
Wireframe Drátový model
|
||||
No Shading Bez stínování
|
||||
Plastic Plast
|
||||
Highlight Intensity Intenzita odlesku
|
||||
Highlight Size Velikost odlesku
|
||||
Blend Steps Kroky prolnutí
|
||||
Shading Color Barva stínování
|
||||
Draw Hidden Faces Kreslit skryté stěny
|
||||
Revolve… Rotační těleso…
|
||||
Inflate… Nafouknutí…
|
||||
Materials… Materiály…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -415,6 +415,17 @@
|
||||
Vector Effects Vektoreffekter
|
||||
3D and Materials 3D og materialer
|
||||
Extrude & Bevel… Ekstrudér og facet…
|
||||
3D (Classic) 3D (klassisk)
|
||||
Extrude & Bevel (Classic)… Ekstrudér og facet (klassisk)…
|
||||
Surface Overflade
|
||||
Wireframe Trådmodel
|
||||
No Shading Ingen skyggelægning
|
||||
Plastic Plastik
|
||||
Highlight Intensity Højlysintensitet
|
||||
Highlight Size Højlysstørrelse
|
||||
Blend Steps Overgangstrin
|
||||
Shading Color Skyggefarve
|
||||
Draw Hidden Faces Tegn skjulte flader
|
||||
Revolve… Omdrej…
|
||||
Inflate… Pust op…
|
||||
Materials… Materialer…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1079,6 +1079,17 @@ font-technology Variable Variabel
|
||||
Exporting {} {} wird exportiert
|
||||
Export… Exportieren…
|
||||
Extrude & Bevel… Extrudieren und abgeflachte Kante…
|
||||
3D (Classic) 3D (klassisch)
|
||||
Extrude & Bevel (Classic)… Extrudieren und abgeflachte Kante (klassisch)…
|
||||
Surface Oberfläche
|
||||
Wireframe Drahtmodell
|
||||
No Shading Keine Schattierung
|
||||
Plastic Kunststoff
|
||||
Highlight Intensity Glanzpunktintensität
|
||||
Highlight Size Glanzpunktgröße
|
||||
Blend Steps Angleichungsstufen
|
||||
Shading Color Schattierungsfarbe
|
||||
Draw Hidden Faces Verdeckte Flächen zeichnen
|
||||
Eyedropper Pipette
|
||||
Sample Color from Screen Farbe vom Bildschirm aufnehmen
|
||||
This system has no screen color sampler Dieses System hat keine Farbaufnahme vom Bildschirm
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1052,6 +1052,17 @@ font-technology Variable Μεταβλητές
|
||||
Exporting {} Εξαγωγή {}
|
||||
Export… Εξαγωγή…
|
||||
Extrude & Bevel… Εξώθηση και λοξότμηση…
|
||||
3D (Classic) 3D (κλασικό)
|
||||
Extrude & Bevel (Classic)… Εξώθηση και λοξότμηση (κλασικό)…
|
||||
Surface Επιφάνεια
|
||||
Wireframe Συρματόπλεγμα
|
||||
No Shading Χωρίς σκίαση
|
||||
Plastic Πλαστικό
|
||||
Highlight Intensity Ένταση ανταύγειας
|
||||
Highlight Size Μέγεθος ανταύγειας
|
||||
Blend Steps Βήματα ανάμειξης
|
||||
Shading Color Χρώμα σκίασης
|
||||
Draw Hidden Faces Σχεδίαση κρυφών εδρών
|
||||
Eyedropper Σταγονόμετρο (Eyedropper Tool)
|
||||
Eyedropper Applies: Το σταγονόμετρο εφαρμόζει:
|
||||
Eyedropper Options Επιλογές σταγονόμετρου
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 163 and column 5.
|
@@ -1108,6 +1108,17 @@ font-technology Variable Variables
|
||||
Exporting {} Exportando {}
|
||||
Export… Exportar…
|
||||
Extrude & Bevel… Extrusión y biselado…
|
||||
3D (Classic) 3D (clásico)
|
||||
Extrude & Bevel (Classic)… Extrusión y biselado (clásico)…
|
||||
Surface Superficie
|
||||
Wireframe Malla
|
||||
No Shading Sin sombreado
|
||||
Plastic Plástico
|
||||
Highlight Intensity Intensidad del brillo
|
||||
Highlight Size Tamaño del brillo
|
||||
Blend Steps Pasos de fusión
|
||||
Shading Color Color de sombreado
|
||||
Draw Hidden Faces Dibujar caras ocultas
|
||||
Eyedropper Cuentagotas
|
||||
Sample Color from Screen Tomar color de la pantalla
|
||||
This system has no screen color sampler Este sistema no tiene muestreador de color de la pantalla
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -454,6 +454,17 @@
|
||||
Vector Effects Vektoritehosteet
|
||||
3D and Materials 3D ja materiaalit
|
||||
Extrude & Bevel… Pursota ja viistä…
|
||||
3D (Classic) 3D (perinteinen)
|
||||
Extrude & Bevel (Classic)… Pursota ja viistä (perinteinen)…
|
||||
Surface Pinta
|
||||
Wireframe Rautalanka
|
||||
No Shading Ei varjostusta
|
||||
Plastic Muovi
|
||||
Highlight Intensity Kiillon voimakkuus
|
||||
Highlight Size Kiillon koko
|
||||
Blend Steps Sulautusvaiheet
|
||||
Shading Color Varjostusväri
|
||||
Draw Hidden Faces Piirrä piilotetut pinnat
|
||||
Revolve… Pyöräytä…
|
||||
Inflate… Täytä ilmalla…
|
||||
Materials… Materiaalit…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1103,6 +1103,17 @@ font-technology Variable Variables
|
||||
Exporting {} Exportation de {}
|
||||
Export… Exporter…
|
||||
Extrude & Bevel… Extrusion et biseautage…
|
||||
3D (Classic) 3D (classique)
|
||||
Extrude & Bevel (Classic)… Extrusion et biseautage (classique)…
|
||||
Surface Surface
|
||||
Wireframe Fil de fer
|
||||
No Shading Sans ombrage
|
||||
Plastic Plastique
|
||||
Highlight Intensity Intensité de la brillance
|
||||
Highlight Size Taille de la brillance
|
||||
Blend Steps Étapes de transition
|
||||
Shading Color Couleur d’ombrage
|
||||
Draw Hidden Faces Dessiner les faces cachées
|
||||
Eyedropper Pipette
|
||||
Sample Color from Screen Prélever une couleur à l’écran
|
||||
This system has no screen color sampler Ce système n’a pas d’outil de prélèvement de couleur à l’écran
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1665,6 +1665,17 @@
|
||||
Extension Lines एक्सटेंशन लाइन
|
||||
Extension: विस्तार:
|
||||
Extrude & Bevel… एक्सट्रूडेड और बेवेल…
|
||||
3D (Classic) 3D (क्लासिक)
|
||||
Extrude & Bevel (Classic)… एक्सट्रूडेड और बेवेल (क्लासिक)…
|
||||
Surface सतह
|
||||
Wireframe वायरफ़्रेम
|
||||
No Shading कोई शेडिंग नहीं
|
||||
Plastic प्लास्टिक
|
||||
Highlight Intensity हाइलाइट तीव्रता
|
||||
Highlight Size हाइलाइट आकार
|
||||
Blend Steps ब्लेंड चरण
|
||||
Shading Color शेडिंग रंग
|
||||
Draw Hidden Faces छिपे हुए फलक बनाएँ
|
||||
Eyedropper आईड्रॉपर
|
||||
Eyedropper Applies: आईड्रॉपपर लागू होता है:
|
||||
Eyedropper Options आईड्रॉपर ऑप्शन्स
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1388,6 +1388,17 @@ font-technology Variable Változó
|
||||
Exporting {} Exportálás: {}
|
||||
Export… Exportálás…
|
||||
Extrude & Bevel… Kihúzás és élletörés…
|
||||
3D (Classic) 3D (klasszikus)
|
||||
Extrude & Bevel (Classic)… Kihúzás és élletörés (klasszikus)…
|
||||
Surface Felület
|
||||
Wireframe Drótváz
|
||||
No Shading Nincs árnyékolás
|
||||
Plastic Műanyag
|
||||
Highlight Intensity Csillanás erőssége
|
||||
Highlight Size Csillanás mérete
|
||||
Blend Steps Átmenetlépések
|
||||
Shading Color Árnyékolás színe
|
||||
Draw Hidden Faces Rejtett lapok rajzolása
|
||||
Eyedropper Pipetta
|
||||
Sample Color from Screen Szín mintavétele a képernyőről
|
||||
This system has no screen color sampler Ezen a rendszeren nincs képernyős színmintavevő
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -415,6 +415,17 @@
|
||||
Vector Effects Efek vektor
|
||||
3D and Materials 3D dan material
|
||||
Extrude & Bevel… Ekstrusi & bevel…
|
||||
3D (Classic) 3D (klasik)
|
||||
Extrude & Bevel (Classic)… Ekstrusi & bevel (klasik)…
|
||||
Surface Permukaan
|
||||
Wireframe Kerangka kawat
|
||||
No Shading Tanpa pengarsiran
|
||||
Plastic Plastik
|
||||
Highlight Intensity Intensitas kilau
|
||||
Highlight Size Ukuran kilau
|
||||
Blend Steps Langkah campuran
|
||||
Shading Color Warna pengarsiran
|
||||
Draw Hidden Faces Gambar sisi tersembunyi
|
||||
Revolve… Revolusi…
|
||||
Inflate… Gelembungkan…
|
||||
Materials… Material…
|
||||
|
||||
|
@@ -1109,6 +1109,17 @@ font-technology Variable Variabili
|
||||
Exporting {} Esportazione di {} in corso
|
||||
Export… Esporta…
|
||||
Extrude & Bevel… Estrusione e smusso…
|
||||
3D (Classic) 3D (classico)
|
||||
Extrude & Bevel (Classic)… Estrusione e smusso (classico)…
|
||||
Surface Superficie
|
||||
Wireframe Reticolo
|
||||
No Shading Nessuna ombreggiatura
|
||||
Plastic Plastica
|
||||
Highlight Intensity Intensità lucentezza
|
||||
Highlight Size Dimensione lucentezza
|
||||
Blend Steps Passaggi di fusione
|
||||
Shading Color Colore ombreggiatura
|
||||
Draw Hidden Faces Disegna facce nascoste
|
||||
Eyedropper Contagocce
|
||||
Sample Color from Screen Campiona colore dallo schermo
|
||||
This system has no screen color sampler Questo sistema non ha un campionatore di colore dello schermo
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -488,6 +488,17 @@ digits Malayalam マラヤーラム数字
|
||||
Vector Effects ベクター効果
|
||||
3D and Materials 3D とマテリアル
|
||||
Extrude & Bevel… 押し出しとベベル…
|
||||
3D (Classic) 3D(クラシック)
|
||||
Extrude & Bevel (Classic)… 押し出しとベベル(クラシック)…
|
||||
Surface 表面
|
||||
Wireframe ワイヤーフレーム
|
||||
No Shading 陰影なし
|
||||
Plastic プラスチック
|
||||
Highlight Intensity ハイライトの強さ
|
||||
Highlight Size ハイライトのサイズ
|
||||
Blend Steps ブレンドの階調
|
||||
Shading Color 陰影のカラー
|
||||
Draw Hidden Faces 隠れた面を描画
|
||||
Revolve… 回転体…
|
||||
Inflate… 膨張…
|
||||
Materials… マテリアル…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -381,6 +381,17 @@
|
||||
Vector Effects ვექტორული ეფექტები
|
||||
3D and Materials 3D და მასალები
|
||||
Extrude & Bevel… ამოწევა და წაკვეთა…
|
||||
3D (Classic) 3D (კლასიკური)
|
||||
Extrude & Bevel (Classic)… ამოწევა და წაკვეთა (კლასიკური)…
|
||||
Surface ზედაპირი
|
||||
Wireframe მავთულის ჩარჩო
|
||||
No Shading დაჩრდილვის გარეშე
|
||||
Plastic პლასტიკური
|
||||
Highlight Intensity ელვარების ინტენსივობა
|
||||
Highlight Size ელვარების ზომა
|
||||
Blend Steps შერევის საფეხურები
|
||||
Shading Color დაჩრდილვის ფერი
|
||||
Draw Hidden Faces დამალული წახნაგების დახატვა
|
||||
Revolve… ბრუნვა…
|
||||
Inflate… გაბერვა…
|
||||
Materials… მასალები…
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -481,6 +481,17 @@ digits Malayalam 말라얄람어
|
||||
Vector Effects 벡터 효과
|
||||
3D and Materials 3D 및 재질
|
||||
Extrude & Bevel… 돌출 및 경사…
|
||||
3D (Classic) 3D(클래식)
|
||||
Extrude & Bevel (Classic)… 돌출 및 경사(클래식)…
|
||||
Surface 표면
|
||||
Wireframe 와이어프레임
|
||||
No Shading 셰이딩 없음
|
||||
Plastic 플라스틱
|
||||
Highlight Intensity 하이라이트 강도
|
||||
Highlight Size 하이라이트 크기
|
||||
Blend Steps 혼합 단계
|
||||
Shading Color 셰이딩 색상
|
||||
Draw Hidden Faces 숨겨진 면 그리기
|
||||
Revolve… 회전체…
|
||||
Inflate… 부풀리기…
|
||||
Materials… 재질…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -429,6 +429,17 @@
|
||||
Vector Effects Vectoreffecten
|
||||
3D and Materials 3D en materialen
|
||||
Extrude & Bevel… Extruderen en afschuinen…
|
||||
3D (Classic) 3D (klassiek)
|
||||
Extrude & Bevel (Classic)… Extruderen en afschuinen (klassiek)…
|
||||
Surface Oppervlak
|
||||
Wireframe Draadmodel
|
||||
No Shading Geen arcering
|
||||
Plastic Plastic
|
||||
Highlight Intensity Glansintensiteit
|
||||
Highlight Size Glansgrootte
|
||||
Blend Steps Overvloeistappen
|
||||
Shading Color Arceringskleur
|
||||
Draw Hidden Faces Verborgen vlakken tekenen
|
||||
Revolve… Draaien…
|
||||
Inflate… Opblazen…
|
||||
Materials… Materialen…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -417,6 +417,17 @@
|
||||
Vector Effects Efekty wektorowe
|
||||
3D and Materials 3D i materiały
|
||||
Extrude & Bevel… Wyciągnięcie i fazowanie…
|
||||
3D (Classic) 3D (klasyczne)
|
||||
Extrude & Bevel (Classic)… Wyciągnięcie i fazowanie (klasyczne)…
|
||||
Surface Powierzchnia
|
||||
Wireframe Szkielet
|
||||
No Shading Bez cieniowania
|
||||
Plastic Plastik
|
||||
Highlight Intensity Intensywność połysku
|
||||
Highlight Size Rozmiar połysku
|
||||
Blend Steps Kroki przejścia
|
||||
Shading Color Kolor cieniowania
|
||||
Draw Hidden Faces Rysuj ukryte ściany
|
||||
Revolve… Obrót…
|
||||
Inflate… Nadmuchanie…
|
||||
Materials… Materiały…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1001,6 +1001,17 @@
|
||||
Export: Exportar:
|
||||
Export… Exportar…
|
||||
Extrude & Bevel… Extrusão e Bisel…
|
||||
3D (Classic) 3D (Clássico)
|
||||
Extrude & Bevel (Classic)… Extrusão e Bisel (Clássico)…
|
||||
Surface Superfície
|
||||
Wireframe Aramado
|
||||
No Shading Sem Sombreamento
|
||||
Plastic Plástico
|
||||
Highlight Intensity Intensidade do Realce
|
||||
Highlight Size Tamanho do Realce
|
||||
Blend Steps Etapas de Mesclagem
|
||||
Shading Color Cor do Sombreamento
|
||||
Draw Hidden Faces Desenhar Faces Ocultas
|
||||
Eyedropper Conta-gotas
|
||||
Eyedropper Applies: O Conta-gotas Aplica:
|
||||
Eyedropper Options Opções do Conta-gotas
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1106,6 +1106,17 @@ font-technology Variable Вариативные
|
||||
Exporting {} Экспорт {}
|
||||
Export… Экспорт…
|
||||
Extrude & Bevel… Выдавливание и фаска…
|
||||
3D (Classic) 3D (классические)
|
||||
Extrude & Bevel (Classic)… Выдавливание и фаска (классическое)…
|
||||
Surface Поверхность
|
||||
Wireframe Каркас
|
||||
No Shading Без затенения
|
||||
Plastic Пластик
|
||||
Highlight Intensity Интенсивность блика
|
||||
Highlight Size Размер блика
|
||||
Blend Steps Шаги перехода
|
||||
Shading Color Цвет затенения
|
||||
Draw Hidden Faces Рисовать скрытые грани
|
||||
Eyedropper Пипетка
|
||||
Sample Color from Screen Взять цвет с экрана
|
||||
This system has no screen color sampler В этой системе нет средства выбора цвета с экрана
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1087,6 +1087,17 @@ font-technology Variable Variabla
|
||||
Exporting {} Exporterar {}
|
||||
Export… Exportera…
|
||||
Extrude & Bevel… Extrudera och fasa…
|
||||
3D (Classic) 3D (klassisk)
|
||||
Extrude & Bevel (Classic)… Extrudera och fasa (klassisk)…
|
||||
Surface Yta
|
||||
Wireframe Trådram
|
||||
No Shading Ingen skuggning
|
||||
Plastic Plast
|
||||
Highlight Intensity Högdagerintensitet
|
||||
Highlight Size Högdagerstorlek
|
||||
Blend Steps Övergångssteg
|
||||
Shading Color Skuggningsfärg
|
||||
Draw Hidden Faces Rita dolda ytor
|
||||
Eyedropper Pipett
|
||||
Eyedropper Applies: Pipetten tillämpar:
|
||||
Eyedropper Options Pipettalternativ
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -1079,6 +1079,17 @@ font-technology Variable Değişken
|
||||
Exporting {} {} dışa aktarılıyor
|
||||
Export… İhracat…
|
||||
Extrude & Bevel… Ekstrüzyon ve Eğim…
|
||||
3D (Classic) 3D (Klasik)
|
||||
Extrude & Bevel (Classic)… Ekstrüzyon ve Eğim (Klasik)…
|
||||
Surface Yüzey
|
||||
Wireframe Tel Kafes
|
||||
No Shading Gölgeleme Yok
|
||||
Plastic Plastik
|
||||
Highlight Intensity Parlama Yoğunluğu
|
||||
Highlight Size Parlama Boyutu
|
||||
Blend Steps Karışım Adımları
|
||||
Shading Color Gölgeleme Rengi
|
||||
Draw Hidden Faces Gizli Yüzleri Çiz
|
||||
Eyedropper Damlalık
|
||||
Sample Color from Screen Ekrandan Örnek Renk
|
||||
This system has no screen color sampler Bu sistemin ekran rengi örnekleyicisi yok
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -1103,6 +1103,17 @@ font-technology Variable Варіативні
|
||||
Exporting {} Експорт {}
|
||||
Export… Експорт…
|
||||
Extrude & Bevel… Видавлювання та фаска…
|
||||
3D (Classic) 3D (класичні)
|
||||
Extrude & Bevel (Classic)… Видавлювання та фаска (класичне)…
|
||||
Surface Поверхня
|
||||
Wireframe Каркас
|
||||
No Shading Без затінення
|
||||
Plastic Пластик
|
||||
Highlight Intensity Інтенсивність відблиску
|
||||
Highlight Size Розмір відблиску
|
||||
Blend Steps Кроки переходу
|
||||
Shading Color Колір затінення
|
||||
Draw Hidden Faces Малювати приховані грані
|
||||
Eyedropper Піпетка
|
||||
Sample Color from Screen Взяти колір з екрана
|
||||
This system has no screen color sampler У цій системі немає засобу вибору кольору з екрана
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 177 and column 5.
|
@@ -1265,6 +1265,17 @@ font-technology Variable Biến thiên
|
||||
Exporting {} Đang xuất {}
|
||||
Export… Xuất…
|
||||
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)…
|
||||
Surface Bề mặt
|
||||
Wireframe Khung dây
|
||||
No Shading Không tô bóng
|
||||
Plastic Nhựa
|
||||
Highlight Intensity Cường độ điểm sáng
|
||||
Highlight Size Kích thước điểm sáng
|
||||
Blend Steps Số bước hòa trộn
|
||||
Shading Color Màu tô bóng
|
||||
Draw Hidden Faces Vẽ các mặt ẩn
|
||||
Eyedropper Ống hút màu
|
||||
Sample Color from Screen Lấy mẫu màu từ màn hình
|
||||
This system has no screen color sampler Hệ thống này không có công cụ lấy mẫu màu trên màn hình
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 199 and column 5.
|
@@ -1049,6 +1049,17 @@
|
||||
Exporting {} 正在导出 {}
|
||||
Export… 导出…
|
||||
Extrude & Bevel… 突出与斜角…
|
||||
3D (Classic) 3D (经典)
|
||||
Extrude & Bevel (Classic)… 突出与斜角 (经典)…
|
||||
Surface 表面
|
||||
Wireframe 线框
|
||||
No Shading 无底纹
|
||||
Plastic 塑料
|
||||
Highlight Intensity 高光强度
|
||||
Highlight Size 高光大小
|
||||
Blend Steps 混合步骤
|
||||
Shading Color 底纹颜色
|
||||
Draw Hidden Faces 绘制隐藏表面
|
||||
Eyedropper 吸管
|
||||
Sample Color from Screen 从屏幕取样颜色
|
||||
This system has no screen color sampler 此系统没有屏幕取色工具
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1073,6 +1073,17 @@ font-technology Variable 可變
|
||||
Exporting {} 正在匯出 {}
|
||||
Export… 匯出…
|
||||
Extrude & Bevel… 突出與斜角…
|
||||
3D (Classic) 3D (經典)
|
||||
Extrude & Bevel (Classic)… 突出與斜角 (經典)…
|
||||
Surface 表面
|
||||
Wireframe 線框
|
||||
No Shading 無網底
|
||||
Plastic 塑膠
|
||||
Highlight Intensity 亮部強度
|
||||
Highlight Size 亮部大小
|
||||
Blend Steps 漸變階數
|
||||
Shading Color 網底顏色
|
||||
Draw Hidden Faces 繪製隱藏表面
|
||||
Eyedropper 滴管
|
||||
Sample Color from Screen 從螢幕取樣顏色
|
||||
This system has no screen color sampler 此系統沒有螢幕取色工具
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -108,6 +108,9 @@ pub(super) fn font_items(english_names: bool) -> Vec<Item> {
|
||||
items.3.clone()
|
||||
}
|
||||
|
||||
/// The catalogue's menu path of the effects in 3D and Materials › 3D (Classic).
|
||||
const CLASSIC_3D: &[&str] = &["Effect", "3D and Materials", "3D (Classic)"];
|
||||
|
||||
/// The raster effects' submenus of the Effect menu (the Photoshop-style effects, below the
|
||||
/// vector effects): (submenu, the catalogue's menu path of its effects).
|
||||
/// The raster Stylize submenu shares its path with the vector one and lists only the effects at
|
||||
@@ -190,6 +193,10 @@ pub(super) fn effect_menu() -> Vec<Item> {
|
||||
_ => 9,
|
||||
});
|
||||
items.push(todo("Materials…"));
|
||||
let classic: Vec<Item> = cat.iter().filter(|e| e.menu == CLASSIC_3D).map(item).collect();
|
||||
if !classic.is_empty() {
|
||||
items.push(sub("3D (Classic)", classic));
|
||||
}
|
||||
}
|
||||
out.push(sub(sub_name, items));
|
||||
}
|
||||
@@ -210,7 +217,9 @@ pub(super) fn effect_menu() -> Vec<Item> {
|
||||
out.extend(raster);
|
||||
}
|
||||
// Anything not placed above (future effects) still shows up.
|
||||
let placed = |e: &vectorcraft_effects::EffectInfo| top_level(e) || in_vector_menu(e) || RASTER_MENUS.iter().any(|(_, path)| e.menu == *path);
|
||||
let placed = |e: &vectorcraft_effects::EffectInfo| {
|
||||
top_level(e) || in_vector_menu(e) || e.menu == CLASSIC_3D || RASTER_MENUS.iter().any(|(_, path)| e.menu == *path)
|
||||
};
|
||||
for e in cat.iter().filter(|e| !placed(e)) {
|
||||
out.push(Item::Cmd(e.label, "effect.dialog", json!({ "effect": e.id })));
|
||||
}
|
||||
|
||||
@@ -354,6 +354,22 @@ fn saved_views_store_and_restore_the_view() {
|
||||
assert!(!menu_entries(&app).iter().any(|e| e.command.as_deref() == Some("view.goto2")));
|
||||
}
|
||||
|
||||
/// Effect › 3D and Materials › 3D (Classic) › Extrude & Bevel (Classic)… opens its effect dialog,
|
||||
/// after Materials…, and nowhere else.
|
||||
#[test]
|
||||
fn classic_extrude_bevel_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.classicExtrude")).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()), ("Extrude & Bevel (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("Materials…").unwrap() < at("Extrude & Bevel (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,7 +181,7 @@ 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), Position presets, rotation and light gizmos, SVG/PDF output, Expand Appearance ([three-d.md](three-d.md)) |
|
||||
| Materials panel, lighting, ray-traced render | 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; no materials, shadows or ray tracing |
|
||||
| 3D (Classic): Extrude & Bevel, Revolve, Rotate | 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), Rotate (Classic) and Map Art: none |
|
||||
| Turntable (30.0 beta) | none |
|
||||
| Export 3D objects (OBJ, USDA, glTF) | none |
|
||||
|
||||
|
||||
+27
@@ -2165,6 +2165,33 @@ All three take `rotationX/Y/Z` (degrees, -180..180, default the Off-Axis Front p
|
||||
`perspectiveAngle` and the lighting parameters as Revolve. See [three-d.md](three-d.md) for the Position presets,
|
||||
the scope and what differs from the public documentation.
|
||||
|
||||
## Extrude & Bevel (Classic)
|
||||
|
||||
`effect.apply {effect: "threeD.classicExtrude", params: {...}, ids?, item?}` is Effect › 3D and Materials ›
|
||||
3D (Classic) › Extrude & Bevel (Classic)…. It builds the same solid as `threeD.extrude` and draws its faces with the
|
||||
classic surfaces. Parameters: `depth` (points, 0..2000, default 50), `cap` (true solid, false hollow), `bevel` (`none`,
|
||||
`classic`, `rounded`, `cove`, `step`, `ogee`), `bevelHeight` (points, 1..100, default 4; the bevel is as wide as it
|
||||
is tall), `bevelInside` (false adds the bevel around the shape, true carves it into the shape), `rotationX/Y/Z` and
|
||||
`perspectiveAngle` as above, and the surface:
|
||||
|
||||
- `surface`: `wireframe` (the edges of every face, hidden faces included, with no fill), `none` (the art's own paint,
|
||||
unlit), `diffuse` (soft light) or `plastic` (soft light plus a highlight; the default).
|
||||
- `lightRotation`, `lightHeight`, `lightBehind` and `lightIntensity` (0..100%, default 100) place one light, or
|
||||
`lights` holds one to ten (`intensity`, `rotation`, `height`, `behind`; their light adds up, and `color` and
|
||||
`softness` are not used). `ambient` (0..100%, default 50) lights every face.
|
||||
- `highlightIntensity` (0..100%, default 60) and `highlightSize` (0..100%, default 90; 100 is broad) shape the Plastic
|
||||
highlight. `blendSteps` (whole number, 1..256, default 25) is how many levels the shading is rounded to.
|
||||
- `shadingColor`: `black` (the default), `custom` (darkens toward `shadingCustom`, a `#rrggbb` color) or `none` (the
|
||||
paint is kept and only the highlight lightens it). `drawHiddenFaces` also draws the faces turned away from the
|
||||
viewer, behind the others.
|
||||
|
||||
The defaults are VectorCraft's own. An unknown parameter, a number out of range, a flag that is not true or false, or
|
||||
an unknown 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.classicExtrude","item":null,"params":{"depth":40,"bevel":"rounded","bevelHeight":6,"surface":"diffuse","blendSteps":64}}}}
|
||||
```
|
||||
|
||||
|
||||
## Edit Colors and Recolor Artwork
|
||||
|
||||
|
||||
Reference in New Issue
Block a user