mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 21:57:04 +00:00
Merge pull request #7829 from dotCooCoo/fix/shadow-validation-builds-its-shadow-geometry
3D: shadow validation casts from the solids it checks
This commit is contained in:
@@ -324,8 +324,7 @@ fn validate_solid_for(doc: Option<&Document>, n: &Node, id: &str, p: &Value, at:
|
||||
reshape_members(n, before, &mut shapes);
|
||||
let b = union_bounds(&shapes).ok_or_else(|| format!("{label} requires a group holding a shape with at least two anchors"))?;
|
||||
check_profiles(id, p, &shapes, b)?;
|
||||
check_solids(id, p, &shapes)?;
|
||||
return check_cast(id, p, &shapes);
|
||||
return check_solids(id, p, &shapes);
|
||||
}
|
||||
let (path, rule) = object_profile(n, label)?;
|
||||
let anchors = || format!("{label} requires a shape with at least two anchors");
|
||||
@@ -342,38 +341,68 @@ fn validate_solid_for(doc: Option<&Document>, n: &Node, id: &str, p: &Value, at:
|
||||
let b = path.bounds().ok_or_else(anchors)?;
|
||||
let shapes = [Profile { path, rule, items, opacity: 1.0, scopes: vec![] }];
|
||||
check_profiles(id, p, &shapes, b)?;
|
||||
check_solids(id, p, &shapes)?;
|
||||
check_cast(id, p, &shapes)
|
||||
check_solids(id, p, &shapes)
|
||||
}
|
||||
|
||||
/// With the shadow of 3D effect `id` with parameters `p` on, an error when the shadow of `shapes`
|
||||
/// is past the limits ([`cast_shadow`]). In wireframe mode no shadow is drawn, and none is checked.
|
||||
fn check_cast(id: &str, p: &Value, shapes: &[Profile]) -> Result<(), String> {
|
||||
if wireframe::wireframe_params(id, p) {
|
||||
return Ok(());
|
||||
}
|
||||
cast_shadow(id, p, shapes).map(|_| ())
|
||||
/// Does 3D effect `id` with parameters `p` cast a shadow: is it Extrude & Bevel, Inflate or Rotate
|
||||
/// with Shadows on, shaded? In wireframe mode no shadow is drawn, and none is checked.
|
||||
fn casts_shadow(id: &str, p: &Value) -> bool {
|
||||
!is_classic_three_d(id) && shadow::options(p).is_some() && !wireframe::wireframe_params(id, p)
|
||||
}
|
||||
|
||||
/// Does `item` of a shape with opacity `member` paint anything, and so cast a shadow?
|
||||
fn visible(item: &AppearanceItem, member: f32) -> bool {
|
||||
paint_of(item, member).1 > 0.0
|
||||
}
|
||||
|
||||
/// Do the solids 3D effect `id` with parameters `p` builds from the profiles of `shapes` about `b`
|
||||
/// build? Only the profiles no solid of [`check_solids`] is built from are built here: every
|
||||
/// shape's for Rotate, and for Extrude & Bevel and Inflate the profile of a shape with no fill,
|
||||
/// whose solids are its strokes' outlines or none. A filled shape's profile is its fill's solid,
|
||||
/// which [`check_solids`] builds once, about the bounds the effect draws it about.
|
||||
/// build? Only the profiles no solid of [`check_solids`] is built from are built here: for Rotate
|
||||
/// every shape's, except with its shadow cast that of a shape with a visible fill, whose plane is
|
||||
/// the fill's; for Extrude & Bevel and Inflate the profile of a shape with no fill, whose solids
|
||||
/// are its strokes' outlines or none. A filled shape's profile is its fill's solid, which
|
||||
/// [`check_solids`] builds once, about the bounds the effect draws it about.
|
||||
fn check_profiles(id: &str, p: &Value, shapes: &[Profile], b: Rect) -> Result<(), String> {
|
||||
shapes
|
||||
.iter()
|
||||
.filter(|s| is_plane(id) || !s.items.iter().any(|i| matches!(i, AppearanceItem::Fill(_))))
|
||||
.try_for_each(|s| facets(id, &s.path, s.rule, b, p).map(|_| ()))
|
||||
let cast = casts_shadow(id, p);
|
||||
let fill = |s: &Profile| s.items.iter().any(|i| matches!(i, AppearanceItem::Fill(_)) && (!is_plane(id) || visible(i, s.opacity)));
|
||||
shapes.iter().filter(|s| if is_plane(id) { !(cast && fill(s)) } else { !fill(s) }).try_for_each(|s| facets(id, &s.path, s.rule, b, p).map(|_| ()))
|
||||
}
|
||||
|
||||
/// Do the solids 3D effect `id` with parameters `p` builds from the fills and strokes of `shapes`
|
||||
/// fit, each on its own and all of them together ([`built_solids`])? Rotate's planes are not
|
||||
/// counted.
|
||||
/// fit, each on its own and all of them together ([`built_solids`]), and with its shadow cast, does
|
||||
/// the shadow fit? The shadow is cast from the faces those same builds kept ([`cast_built`]), as
|
||||
/// [`art_for`] casts it: Extrude & Bevel and Inflate build each solid once, and Rotate builds the
|
||||
/// plane of each visible fill and stroke once. Rotate's planes are not counted, and without its
|
||||
/// shadow Rotate builds no plane here.
|
||||
fn check_solids(id: &str, p: &Value, shapes: &[Profile]) -> Result<(), String> {
|
||||
let Some(b) = union_bounds(shapes).filter(|_| !is_plane(id)) else { return Ok(()) };
|
||||
let Some(b) = union_bounds(shapes) else { return Ok(()) };
|
||||
let cast = casts_shadow(id, p);
|
||||
if is_plane(id) {
|
||||
if !cast {
|
||||
return Ok(());
|
||||
}
|
||||
let mut built = vec![];
|
||||
for s in shapes {
|
||||
for item in &s.items {
|
||||
if !visible(item, s.opacity) {
|
||||
built.push(None);
|
||||
continue;
|
||||
}
|
||||
let (area, rule) = region(&s.path, s.rule, item);
|
||||
#[cfg(test)]
|
||||
SOLIDS_BUILT.with(|n| n.set(n.get() + 1));
|
||||
built.push(facets_and_faces(ROTATE, &area, rule, b, p, true)?.1);
|
||||
}
|
||||
}
|
||||
return cast_built(id, p, shapes, built).map(|_| ());
|
||||
}
|
||||
let (parts, bounds) = solid_parts(shapes, b);
|
||||
built_solids(id, p, &parts, bounds, MAX_OBJECT_FACES).try_for_each(|r| r.map(|_| ()))
|
||||
if !cast {
|
||||
return built_solids(id, p, &parts, bounds, MAX_OBJECT_FACES).try_for_each(|r| r.map(|_| ()));
|
||||
}
|
||||
let built = built_solids_cast(id, p, &parts, bounds, MAX_OBJECT_FACES, true)
|
||||
.map(|r| r.map(|solid| solid.and_then(|(_, faces)| faces)))
|
||||
.collect::<Result<Vec<_>, String>>()?;
|
||||
cast_built(id, p, shapes, built).map(|_| ())
|
||||
}
|
||||
|
||||
/// [`profile`], or the error a command reports for object `n`: type with no outlines to build from
|
||||
@@ -733,7 +762,8 @@ thread_local! {
|
||||
/// surfaces count too.
|
||||
pub(crate) static SOLIDS_BUILT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
/// How many Extrude & Bevel (Classic included) and Inflate solids [`facets`] and
|
||||
/// [`built_solids`] have built on this thread, for the tests.
|
||||
/// [`built_solids`] have built on this thread, for the tests. The solids built to cast a shadow
|
||||
/// ([`cast_shadow`]) count too.
|
||||
static SOLID_GEOMETRY_BUILT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
@@ -1435,7 +1465,7 @@ fn art_for(doc: Option<&Document>, n: &Node, fx: &Effect, shapes: &[Profile]) ->
|
||||
return Some((vec![], wireframe::wire_surfaces(n.id, fx, shapes)?));
|
||||
}
|
||||
let p = crate::merged_params(&fx.id, &fx.params);
|
||||
let cast = !is_classic_three_d(&fx.id) && shadow::options(&p).is_some();
|
||||
let cast = casts_shadow(&fx.id, &p);
|
||||
let (art, built) = surfaces_for(doc, n, fx, shapes, cast)?;
|
||||
// A shadow past the limits draws the source path, as a solid past them does; commands report
|
||||
// the error ([`validate_solid_in`]).
|
||||
@@ -1500,12 +1530,16 @@ fn cast_shadow<'a>(id: &str, p: &Value, shapes: &'a [Profile]) -> Result<Option<
|
||||
let casts: Vec<bool> = inks.iter().map(|i| i.casts()).collect();
|
||||
#[cfg(test)]
|
||||
SOLIDS_BUILT.with(|n| n.set(n.get() + shapes.len()));
|
||||
#[cfg(test)]
|
||||
SOLID_GEOMETRY_BUILT.with(|n| n.set(n.get() + casts.iter().filter(|c| **c).count()));
|
||||
caster.inflate(&shapes, &casts, bounds, inflate_options(p), view)
|
||||
} else {
|
||||
let mut added = vec![];
|
||||
for ((_, _, area, rule, _, _), ink) in parts.iter().zip(&inks) {
|
||||
#[cfg(test)]
|
||||
SOLIDS_BUILT.with(|n| n.set(n.get() + usize::from(ink.casts())));
|
||||
#[cfg(test)]
|
||||
SOLID_GEOMETRY_BUILT.with(|n| n.set(n.get() + usize::from(ink.casts())));
|
||||
added.push(if ink.casts() { caster.extrude(area, *rule, bounds, extrude_options(p), view) } else { Ok(0..0) });
|
||||
}
|
||||
added
|
||||
@@ -1529,16 +1563,14 @@ fn cast_shadow<'a>(id: &str, p: &Value, shapes: &'a [Profile]) -> Result<Option<
|
||||
|
||||
/// [`cast_shadow`] from `built`, the faces the solids drawn kept ([`surfaces_for`] with its shadow
|
||||
/// cast), one entry for each solid in the order they are built: for Rotate each fill and stroke of
|
||||
/// each shape, otherwise each part ([`solid_parts`]). A stroke's solid left out of the art (past its
|
||||
/// own limits) kept no faces. Its Extrude & Bevel solid is built for the shadow alone, as
|
||||
/// [`cast_shadow`] builds it, and when an Inflate solid of visible art kept no faces the whole
|
||||
/// shadow is cast as [`cast_shadow`] casts it, since Inflate's solids share one eye.
|
||||
/// each shape, otherwise each part ([`solid_parts`]). No solid is built here: an entry with no faces
|
||||
/// (a stroke's solid left out of the art, past its own limits) casts nothing, as it draws nothing.
|
||||
/// Only `built` of another length than the solids is cast as [`cast_shadow`] casts it.
|
||||
fn cast_built<'a>(id: &str, p: &Value, shapes: &'a [Profile], built: Vec<Option<SolidFaces>>) -> Result<Option<Cast<'a>>, String> {
|
||||
if id == CLASSIC_EXTRUDE || id == CLASSIC_ROTATE {
|
||||
return Ok(None);
|
||||
}
|
||||
let (Some(settings), Some(b)) = (shadow::options(p), union_bounds(shapes)) else { return Ok(None) };
|
||||
let view = view_options(p);
|
||||
let mut caster = vectorcraft_three_d::ShadowCaster::new();
|
||||
let mut cast: Vec<shadow::Cast> = vec![];
|
||||
let ink = |item: &AppearanceItem, member: f32| {
|
||||
@@ -1555,39 +1587,22 @@ fn cast_built<'a>(id: &str, p: &Value, shapes: &'a [Profile], built: Vec<Option<
|
||||
if !ink.casts() {
|
||||
continue;
|
||||
}
|
||||
let faces = match kept {
|
||||
Some(kept) => caster.add_solid(kept)?,
|
||||
None => {
|
||||
let (area, rule) = region(&s.path, s.rule, item);
|
||||
#[cfg(test)]
|
||||
SOLIDS_BUILT.with(|n| n.set(n.get() + 1));
|
||||
caster.plane(&area, rule, b, view)?
|
||||
}
|
||||
};
|
||||
cast.push((s.scopes.as_slice(), ink, faces));
|
||||
let Some(kept) = kept else { continue };
|
||||
cast.push((s.scopes.as_slice(), ink, caster.add_solid(kept)?));
|
||||
}
|
||||
return Ok(Some((caster, settings, cast)));
|
||||
}
|
||||
let (parts, bounds) = solid_parts(shapes, b);
|
||||
let inks: Vec<shadow::Ink> = parts.iter().map(|(item, member, ..)| ink(item, *member)).collect();
|
||||
if parts.len() != built.len() || (id == INFLATE && inks.iter().zip(&built).any(|(ink, kept)| ink.casts() && kept.is_none())) {
|
||||
let (parts, _) = solid_parts(shapes, b);
|
||||
if parts.len() != built.len() {
|
||||
return cast_shadow(id, p, shapes);
|
||||
}
|
||||
for (((item, _, area, rule, scopes, _), ink), kept) in parts.iter().zip(inks).zip(built) {
|
||||
if !ink.casts() {
|
||||
continue;
|
||||
}
|
||||
let faces = match kept {
|
||||
Some(kept) => caster.add_solid(kept),
|
||||
None => {
|
||||
#[cfg(test)]
|
||||
SOLIDS_BUILT.with(|n| n.set(n.get() + 1));
|
||||
caster.extrude(area, *rule, bounds, extrude_options(p), view)
|
||||
}
|
||||
};
|
||||
for ((item, member, _, _, scopes, _), kept) in parts.iter().zip(built) {
|
||||
let ink = ink(item, *member);
|
||||
// A fully transparent solid, or a stroke's solid left out of the art, casts nothing.
|
||||
let Some(kept) = kept.filter(|_| ink.casts()) else { continue };
|
||||
// A stroke whose own solid is past the limits is left out; past the shadow's limit for all
|
||||
// the solids together, the shadow is refused.
|
||||
let faces = match faces {
|
||||
let faces = match caster.add_solid(kept) {
|
||||
Ok(faces) => faces,
|
||||
Err(e) if matches!(item, AppearanceItem::Stroke(_)) && e != vectorcraft_three_d::TOO_COMPLEX_SHADOW => continue,
|
||||
Err(e) => return Err(e),
|
||||
|
||||
@@ -1082,7 +1082,8 @@ fn geometry_builds(check: impl FnOnce() -> Result<(), String>) -> (usize, Result
|
||||
|
||||
/// The check builds each solid the effect draws once, about the bounds it draws it about: one for
|
||||
/// a fill and one for each stroke, on an object, on a group's members and on an appearance item,
|
||||
/// with the shadow on or off.
|
||||
/// with the shadow on or off. With the shadow on, the shadow is cast from the faces those builds
|
||||
/// kept, so no solid is built again for it.
|
||||
#[test]
|
||||
fn the_check_builds_each_drawn_solid_once() {
|
||||
for id in [EXTRUDE, CLASSIC_EXTRUDE, INFLATE] {
|
||||
@@ -1098,6 +1099,11 @@ fn the_check_builds_each_drawn_solid_once() {
|
||||
assert_eq!(geometry_builds(|| validate_three_d_at(&n, id, &json!({}), (Some(0), 0))), (1, Ok(())), "{id}: on the fill");
|
||||
let shadow = json!({"shadow": true});
|
||||
assert_eq!(geometry_builds(|| validate_solid(&n, id, &shadow)), (2, Ok(())), "{id}: with the shadow on");
|
||||
let mut other = square_node();
|
||||
other.id = NodeId(2);
|
||||
let g = Node::group(NodeId(9999), vec![Arc::new(square_node()), Arc::new(other)]);
|
||||
assert_eq!(geometry_builds(|| validate_solid(&g, id, &shadow)), (4, Ok(())), "{id}: a group with the shadow on");
|
||||
assert_eq!(geometry_builds(|| validate_three_d_at(&n, id, &shadow, (Some(0), 0))), (1, Ok(())), "{id}: the fill with the shadow on");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -313,6 +313,68 @@ fn the_shadow_cast_from_the_solids_drawn_is_the_shadow_of_solids_built_for_it()
|
||||
}
|
||||
}
|
||||
|
||||
/// A stroke's solid left out of the art (past its own limits) keeps no faces, and the shadow cast
|
||||
/// from the faces the solids kept leaves it out without building it: it casts nothing, as it draws
|
||||
/// nothing, and the fill's solid still casts.
|
||||
#[test]
|
||||
fn a_stroke_left_out_of_the_art_casts_no_shadow() {
|
||||
for id in [EXTRUDE, INFLATE] {
|
||||
let n = stroked_square(1, 0.0, 1.0);
|
||||
let (path, rule) = profile(&n).unwrap();
|
||||
let shapes = [Profile { path, rule, items: solid_paints(&n), opacity: 1.0, scopes: vec![] }];
|
||||
let p = crate::merged_params(id, &json!({"shadow": true}));
|
||||
let (parts, bounds) = solid_parts(&shapes, union_bounds(&shapes).unwrap());
|
||||
let mut built: Vec<Option<SolidFaces>> =
|
||||
built_solids_cast(id, &p, &parts, bounds, MAX_OBJECT_FACES, true).map(|r| r.unwrap().and_then(|(_, faces)| faces)).collect();
|
||||
let stroke = parts.iter().position(|(item, ..)| matches!(item, AppearanceItem::Stroke(_))).unwrap();
|
||||
assert!(built.len() == 2 && built.iter().all(Option::is_some), "{id}");
|
||||
let (both, _, cast) = cast_built(id, &p, &shapes, built.clone()).unwrap().unwrap();
|
||||
assert_eq!(cast.len(), 2, "{id}: the fill and the stroke cast");
|
||||
built[stroke] = None;
|
||||
let start = SOLIDS_BUILT.with(std::cell::Cell::get);
|
||||
let (fill, _, cast) = cast_built(id, &p, &shapes, built).unwrap().unwrap();
|
||||
assert_eq!(SOLIDS_BUILT.with(std::cell::Cell::get), start, "{id}: no solid is built for the shadow");
|
||||
assert_eq!(cast.len(), 1, "{id}: the stroke left out casts nothing");
|
||||
assert!(!fill.is_empty() && fill.len() < both.len(), "{id}: only the fill's faces are cast");
|
||||
}
|
||||
}
|
||||
|
||||
/// A command accepts each shadowed object that draws with its shadow, and with Rotate builds the
|
||||
/// plane of each visible fill and stroke once for the shadow: a fully transparent one casts nothing.
|
||||
#[test]
|
||||
fn the_check_accepts_the_shadowed_objects_that_draw() {
|
||||
for id in [EXTRUDE, INFLATE, ROTATE] {
|
||||
for params in shadow_settings(id) {
|
||||
for n in shadowed(id, ¶ms) {
|
||||
let mut drawn = 0;
|
||||
art(&n, id).unwrap().walk(&mut |c| drawn += usize::from(shadow_opacity(c).is_some()));
|
||||
assert!(drawn > 0, "{id} {params}");
|
||||
assert_eq!(validate_solid(&n, id, ¶ms), Ok(()), "{id} {params}");
|
||||
}
|
||||
}
|
||||
}
|
||||
let shadow = json!({"shadow": true});
|
||||
for (stroke, planes) in [(1.0, 2), (0.5, 2), (0.0, 1)] {
|
||||
let n = stroked_square(1, 0.0, stroke);
|
||||
let start = SOLIDS_BUILT.with(std::cell::Cell::get);
|
||||
assert_eq!(validate_solid(&n, ROTATE, &shadow), Ok(()), "{stroke}");
|
||||
assert_eq!(SOLIDS_BUILT.with(std::cell::Cell::get) - start, planes, "Rotate, a stroke at {stroke}");
|
||||
}
|
||||
}
|
||||
|
||||
/// In wireframe mode no shadow is drawn, so the check casts none: with Shadows on, Rotate builds no
|
||||
/// plane for a shadow, and Extrude & Bevel and Inflate build each solid once, as with the shadow off.
|
||||
#[test]
|
||||
fn the_check_casts_no_shadow_in_wireframe_mode() {
|
||||
let n = stroked_square(1, 0.0, 1.0);
|
||||
let wire = json!({"shadow": true, "render": {"mode": "wireframe"}});
|
||||
for (id, solids) in [(ROTATE, 0), (EXTRUDE, 2), (INFLATE, 2)] {
|
||||
let start = SOLIDS_BUILT.with(std::cell::Cell::get);
|
||||
assert_eq!(validate_solid(&n, id, &wire), Ok(()), "{id}");
|
||||
assert_eq!(SOLIDS_BUILT.with(std::cell::Cell::get) - start, solids, "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The opacity of each part of the shadow of `n` (its 3D effect's shadow on) and the shadow's art,
|
||||
/// as [`cast_shadow`] casts it from solids built for the shadow alone and as [`cast_built`] casts
|
||||
/// it from the faces the solids drawn kept.
|
||||
|
||||
@@ -592,6 +592,7 @@ since 2026-10-01.
|
||||
- 3D lighting widget: the Lighting tab of the 3D and Materials panel shows every light on a sphere at its Rotation and Height; a drag moves a light in one undo step, a press selects it, Move Light to Back and Move Light to Front change its side, and the arrow keys move the focused light ([three-d.md](three-d.md#lighting-widget)). `crates/ui-egui/src/panels/tests_three_d.rs` covers the drag, selection, side and keyboard.
|
||||
- 3D shadows: Revolve, Extrude & Bevel, Inflate and Rotate take a Shadows switch with Position (Behind Object or Below Object), Distance from Object (0–100 %) and Shadow Bounds (10–400 %) on the Lighting tab ([three-d.md](three-d.md#shadows)); the first light casts one hard-edged vector shadow, drawn behind the solids, in exports and kept by Expand Appearance. Evidence: `crates/three-d/src/shadow/tests.rs`, `crates/effects/src/solids/tests_shadow.rs`, `effectcmd::tests::a_3d_shadow_is_one_undo_step_saved_exported_and_expanded`. Softness and the shadows of lights after the first are not built. Scores unchanged.
|
||||
- 3D shadows: with Shadows on, Revolve, Extrude & Bevel, Inflate and Rotate cast the shadow from the faces kept by the build that draws each solid, so drawing an object builds each of its solids once. The art and the shadow are unchanged. Evidence: `crates/three-d/src/shadow/tests.rs` (`a_solid_and_its_shadow_are_built_once` and the tests that compare each solid and its shadow with the two built apart), `solids::tests_shadow::a_shadowed_3d_object_builds_each_solid_once`, `solids::tests_shadow::the_shadow_cast_from_the_solids_drawn_is_the_shadow_of_solids_built_for_it`, `revolve::tests::the_shadow_cast_from_the_surfaces_drawn_is_the_shadow_of_surfaces_swept_for_it`. Scores unchanged.
|
||||
- 3D shadows: a command that applies or edits Extrude & Bevel, Inflate or Rotate with Shadows on checks the shadow from the faces kept by the builds that check the solids, as drawing casts it, so it builds each solid once instead of building it again for the shadow. A stroke whose solid is left out of the art past its own limits casts no shadow. Evidence: `solids::tests::the_check_builds_each_drawn_solid_once`, `solids::tests_shadow::a_stroke_left_out_of_the_art_casts_no_shadow`, `solids::tests_shadow::the_check_accepts_the_shadowed_objects_that_draw`. Scores unchanged.
|
||||
- 3D shadows: in Revolve, Extrude & Bevel, Inflate and Rotate, a fill, stroke or member whose opacity is NaN or infinite casts no part of the shadow, as one at 0 % does, whether the shadow is cast from the solids drawn or from solids built for it alone; the art beside it still casts. This is VectorCraft's own rule. Evidence: `solids::tests_shadow::art_whose_opacity_is_not_a_finite_number_casts_no_shadow`, `revolve::tests::a_surface_whose_opacity_is_not_a_finite_number_casts_no_shadow`. Scores unchanged.
|
||||
- Window › 3D and Materials panel: `crates/ui-egui/src/panels/three_d.rs` applies a 3D effect by object type (Plane, Extrude, Revolve, Inflate), changes an applied one in place (`effect.setParams` `threeDType`) and edits it with the options dialog's Object, Rotation and Lighting controls, one undo step per change; `crates/engine/src/cmd/effectcmd/three_d_type_tests.rs` and `crates/ui-egui/src/panels/tests_three_d.rs` cover it. No Materials tab.
|
||||
- 3D and Materials › Quick Actions › Expand as Wireframes (#6354): `effect.expandAsWireframes` replaces each selected 3D object with a group of 0.5 pt black stroked paths, the edges of the faces its effect draws, each edge once and joined end to end, curves kept, in one undo step ([three-d.md](three-d.md#3d-and-materials-panel)); `crates/effects/src/solids/wireframe/tests.rs`, `crates/engine/src/cmd/wireframe/tests.rs` and `crates/ui-egui/src/panels/tests_three_d.rs` cover it. Scores and hours unchanged.
|
||||
|
||||
+3
-1
@@ -397,7 +397,9 @@ Our reading:
|
||||
- Softness is not applied to the shadow, and only the first light casts one.
|
||||
- A shadow is cast from at most 327,680 faces, the solids of one object together (our own limit). Past it, a
|
||||
command is refused with "The 3D shadow is too complex; turn Shadows off or simplify the art", and a loaded
|
||||
file draws the source path.
|
||||
file draws the source path. A command checks the shadow from the same builds that check the solids, as
|
||||
drawing casts it, so each solid is built once. A stroke whose solid is left out of the art (past its own
|
||||
limits) casts no shadow.
|
||||
- Canvas, SVG and PDF exports draw the shadow, the art's bounds take it in, and Expand Appearance keeps it as a
|
||||
path below the faces. Extrude & Bevel (Classic), Revolve (Classic) and Rotate (Classic) have no shadow.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user