mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 08:29:14 +00:00
3D and Materials: Expand as Wireframes keeps a layer's ruler guides and reshapes each face
- Expanding a layer removed its ruler guides with its members. The guides of the layer and of its sublayers now stay on the layer, after the edges. - Geometry effects after a 3D effect reshaped all the faces as one path, so Offset Path merged faces that touch. Each effect now reshapes each face on its own, about the bounds of all the faces, within one shared budget of anchors, as it reshapes the drawn surface. Docs: docs/mcp.md.
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
//! Expand as Wireframes: the edges of the faces a 3D effect draws, as open and closed paths that
|
||||
//! can be edited on their own.
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_doc::{AppearanceItem, Document, Effect, Node};
|
||||
use vectorcraft_doc::color::{Color, Paint};
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, Document, Effect, Node, NodeId};
|
||||
use vectorcraft_geom::{Anchor, CubicBez, PathData, Point, SubPath};
|
||||
use vectorcraft_three_d::Facet;
|
||||
|
||||
@@ -10,7 +12,7 @@ use super::{
|
||||
INFLATE, Profile, ROTATE, facets, geometry, has_solid, inflate_options, is_container, is_plane, is_three_d, label_of, members, object_profile,
|
||||
region, reshape_members, resolve, solid, solid_paints, solid_parts, union_bounds, view_options,
|
||||
};
|
||||
use crate::{GeomContext, apply_geometry_with, is_geometry};
|
||||
use crate::{GeomContext, is_geometry};
|
||||
|
||||
/// The most distinct edges one object's wireframe may have.
|
||||
pub(crate) const MAX_WIRE_EDGES: usize = 1_000_000;
|
||||
@@ -50,7 +52,7 @@ pub fn three_d_wireframe_in(doc: Option<&Document>, n: &Node) -> Result<Vec<Path
|
||||
}
|
||||
let n = &*resolve(doc, n, label)?;
|
||||
let faces = if revolve { crate::revolve::revolve_face_paths(n)? } else { solid_face_paths(n)? };
|
||||
let faces = after_geometry(n, faces);
|
||||
let faces = after_geometry(doc, n, faces);
|
||||
let wire = edges(&faces)?;
|
||||
if wire.is_empty() {
|
||||
return Err(format!("{label} draws no faces for this object"));
|
||||
@@ -58,24 +60,30 @@ pub fn three_d_wireframe_in(doc: Option<&Document>, n: &Node) -> Result<Vec<Path
|
||||
Ok(wire)
|
||||
}
|
||||
|
||||
/// `faces` reshaped by the visible geometry effects after `n`'s own 3D effect, which act on the
|
||||
/// projected art as a whole, about the bounds of all the faces.
|
||||
fn after_geometry(n: &Node, faces: Vec<PathData>) -> Vec<PathData> {
|
||||
/// `faces` reshaped by the visible geometry effects after `n`'s own 3D effect ([`reshaped`]).
|
||||
fn after_geometry(doc: Option<&Document>, n: &Node, faces: Vec<PathData>) -> Vec<PathData> {
|
||||
let fx = &n.appearance.effects;
|
||||
let Some(index) = fx.iter().position(|e| e.visible && is_three_d(&e.id)) else { return faces };
|
||||
reshaped(faces, fx.get(index + 1..).unwrap_or_default(), &GeomContext::of(n))
|
||||
reshaped(faces, fx.get(index + 1..).unwrap_or_default(), &GeomContext { doc, ..GeomContext::of(n) })
|
||||
}
|
||||
|
||||
/// `faces` reshaped by the visible geometry effects in `later` (the effects after a 3D effect),
|
||||
/// which act on the faces as a whole, about their bounds, as they act on the drawn surface.
|
||||
/// `faces` reshaped by the visible geometry effects in `later` (the effects after a 3D effect), as
|
||||
/// they reshape the faces of the drawn surface ([`crate::reshape::reshape_leaves`]): each effect
|
||||
/// reshapes each face on its own, about the bounds all the faces have before it, and the faces keep
|
||||
/// to one budget of anchors together. `ctx` gives the object's id, the seed of a random Transform,
|
||||
/// and the document.
|
||||
pub(crate) fn reshaped(faces: Vec<PathData>, later: &[Effect], ctx: &GeomContext) -> Vec<PathData> {
|
||||
let after: Vec<Effect> = later.iter().filter(|e| e.visible && is_geometry(&e.id)).cloned().collect();
|
||||
let after: Vec<&Effect> = later.iter().filter(|e| e.visible && is_geometry(&e.id)).collect();
|
||||
if after.is_empty() {
|
||||
return faces;
|
||||
}
|
||||
let joined = PathData::new(faces.into_iter().flat_map(|f| f.subpaths).collect());
|
||||
let Some(bounds) = joined.bounds() else { return vec![joined] };
|
||||
vec![apply_geometry_with(&after, &joined, bounds, ctx)]
|
||||
let id = NodeId(ctx.seed);
|
||||
let fill = Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0);
|
||||
let mut art = Node::group(id, faces.into_iter().map(|f| Arc::new(Node::path(id, f, fill.clone()))).collect());
|
||||
crate::reshape::reshape_leaves(&mut art, &after, later, ctx.doc);
|
||||
let mut out = vec![];
|
||||
art.walk(&mut |c| out.extend(c.path_data().cloned()));
|
||||
out
|
||||
}
|
||||
|
||||
/// The outline of every face [`super::solid_art`] draws for `n` (already resolved): with the effect
|
||||
|
||||
@@ -157,6 +157,19 @@ fn revolve_classic_and_rotate_classic_expand_too() {
|
||||
assert_eq!(segments(&plane).len(), 4);
|
||||
}
|
||||
|
||||
/// The geometry effects after a 3D effect reshape each face on its own, as they reshape each face
|
||||
/// of the drawn surface: two faces that touch stay two outlines.
|
||||
#[test]
|
||||
fn later_geometry_effects_reshape_each_face_on_its_own() {
|
||||
let n = solid_square(super::super::EXTRUDE, json!({}));
|
||||
let fx = [crate::new_effect("path.offsetPath", &json!({"offset": 1.0})).unwrap()];
|
||||
let out = reshaped(vec![square(0.0, 0.0, 10.0), square(10.0, 0.0, 10.0)], &fx, &GeomContext::of(&n));
|
||||
assert_eq!(out.len(), 2, "{out:?}");
|
||||
let (a, b) = (out[0].bounds().unwrap(), out[1].bounds().unwrap());
|
||||
assert!((a.x0 + 1.0).abs() < 1e-3 && (a.x1 - 11.0).abs() < 1e-3, "{a:?}");
|
||||
assert!((b.x0 - 9.0).abs() < 1e-3 && (b.x1 - 21.0).abs() < 1e-3, "{b:?}");
|
||||
}
|
||||
|
||||
/// A geometry effect after a fill's 3D effect moves that fill's edges, as it moves its drawn surface.
|
||||
#[test]
|
||||
fn a_geometry_effect_after_a_fill_s_3d_effect_reshapes_its_edges() {
|
||||
|
||||
@@ -25,7 +25,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Expand as Wireframes",
|
||||
[],
|
||||
None,
|
||||
"{ids?: [..] (default: the selected objects)} replace each object that has a visible 3D effect (Extrude & Bevel, Revolve, Inflate, Rotate or one of their 3D (Classic) forms, on the object or on its fills and strokes) with a group of paths: the edges of the faces the effect draws, as the object is turned and projected now, each edge once (edges that meet end to end make one path, closed where it comes back to its start; curved edges stay curves; geometry effects after the 3D effect, on the object or on a fill or stroke, reshape them too), stroked 0.5 pt black with round joins and no fill (VectorCraft's own); the group takes the object's place, id, name, opacity, blending mode, opacity mask, attributes, slice and text wrap and is selected (a layer stays a layer holding the edges, which are selected); objects without a visible 3D effect are left as they are; one undo step. An error, with nothing changed, when no object is selected or given, when none has a visible 3D effect, when an object's solid can't be built or the canvas draws its source art in its place, or when one object's wireframe has more than 1,000,000 edges → {ids}",
|
||||
"{ids?: [..] (default: the selected objects)} replace each object that has a visible 3D effect (Extrude & Bevel, Revolve, Inflate, Rotate or one of their 3D (Classic) forms, on the object or on its fills and strokes) with a group of paths: the edges of the faces the effect draws, as the object is turned and projected now, each edge once (edges that meet end to end make one path, closed where it comes back to its start; curved edges stay curves; geometry effects after the 3D effect, on the object or on a fill or stroke, reshape them too), stroked 0.5 pt black with round joins and no fill (VectorCraft's own); the group takes the object's place, id, name, opacity, blending mode, opacity mask, attributes, slice and text wrap and is selected (a layer stays a layer holding the edges, which are selected, and its ruler guides); objects without a visible 3D effect are left as they are; one undo step. An error, with nothing changed, when no object is selected or given, when none has a visible 3D effect, when an object's solid can't be built or the canvas draws its source art in its place, or when one object's wireframe has more than 1,000,000 edges → {ids}",
|
||||
has_doc,
|
||||
expand_as_wireframes
|
||||
)]
|
||||
@@ -38,6 +38,25 @@ fn wire_appearance() -> Appearance {
|
||||
Appearance { items: vec![AppearanceItem::Stroke(stroke)], ..Default::default() }
|
||||
}
|
||||
|
||||
/// The ruler guides of layer `n` and then those of the sublayers inside it, in order, added to
|
||||
/// `out`. A guide from a hidden or locked sublayer, or from one inside a hidden or locked group or
|
||||
/// sublayer, is hidden or locked itself.
|
||||
fn ruler_guides(n: &Node, hidden: bool, locked: bool, out: &mut Vec<Arc<Node>>) {
|
||||
let Some(children) = n.children() else { return };
|
||||
for c in children.iter().filter(|c| c.is_ruler_guide()) {
|
||||
let mut g = c.clone();
|
||||
if hidden || locked {
|
||||
let g = Arc::make_mut(&mut g);
|
||||
g.visible &= !hidden;
|
||||
g.locked |= locked;
|
||||
}
|
||||
out.push(g);
|
||||
}
|
||||
for c in children.iter().filter(|c| !c.is_ruler_guide()) {
|
||||
ruler_guides(c, hidden || !c.visible, locked || c.locked, out);
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_as_wireframes(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let roots = checked_ids_or_selection(s, p, C)?;
|
||||
if roots.is_empty() {
|
||||
@@ -70,9 +89,13 @@ fn expand_as_wireframes(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let Some(source) = d.node(id) else { continue };
|
||||
if matches!(source.kind, NodeKind::Layer { .. }) {
|
||||
// A layer stays a layer: its members become the edges, and its 3D effect is removed.
|
||||
// Its ruler guides and those of its sublayers are not art: they stay, after the edges.
|
||||
let mut layer = source.clone();
|
||||
let children: Vec<Arc<Node>> = wire.into_iter().map(|path| Arc::new(Node::path(d.alloc_id(), path, wire_appearance()))).collect();
|
||||
let mut guides = vec![];
|
||||
ruler_guides(source, false, false, &mut guides);
|
||||
let mut children: Vec<Arc<Node>> = wire.into_iter().map(|path| Arc::new(Node::path(d.alloc_id(), path, wire_appearance()))).collect();
|
||||
selected.extend(children.iter().map(|c| c.id));
|
||||
children.extend(guides);
|
||||
if let Some(members) = layer.children_mut() {
|
||||
*members = children;
|
||||
}
|
||||
|
||||
@@ -92,6 +92,26 @@ fn a_layer_with_a_3d_effect_stays_a_layer_holding_the_edges() {
|
||||
assert_eq!(undo_len(&s), history + 1);
|
||||
}
|
||||
|
||||
/// A layer's ruler guides are not art: they stay on the layer, after the edges, and are not selected.
|
||||
#[test]
|
||||
fn a_layer_keeps_its_ruler_guides() {
|
||||
let (mut s, _) = setup(None);
|
||||
let layer = s.doc().unwrap().doc.layers[0].id;
|
||||
s.execute("guide.add", &json!({"vertical": true, "pos": 50})).unwrap();
|
||||
let guides = |s: &Session| -> Vec<NodeId> {
|
||||
s.doc().unwrap().doc.node(layer).unwrap().children().unwrap().iter().filter(|c| c.is_ruler_guide()).map(|c| c.id).collect()
|
||||
};
|
||||
let before = guides(&s);
|
||||
assert_eq!(before.len(), 1);
|
||||
s.execute("effect.apply", &json!({"ids":[layer.0],"effect":"threeD.extrude","item":null,"params":{}})).unwrap();
|
||||
run(&mut s, json!({"ids":[layer.0]})).unwrap();
|
||||
assert_eq!(guides(&s), before);
|
||||
let st = s.doc().unwrap();
|
||||
let children = st.doc.node(layer).unwrap().children().unwrap();
|
||||
assert!(children.len() > 1 && children.last().is_some_and(|c| c.is_ruler_guide()));
|
||||
assert!(!st.selection.objects.is_empty() && st.selection.objects.iter().all(|id| !before.contains(id)));
|
||||
}
|
||||
|
||||
/// The number of segments in the wireframe group `id`.
|
||||
fn segments(s: &Session, id: NodeId) -> usize {
|
||||
let st = s.doc().unwrap();
|
||||
|
||||
+1
-1
@@ -3237,7 +3237,7 @@ on the object or on a fill or stroke, reshape its edges as they reshape the draw
|
||||
black stroke with round joins and no fill (VectorCraft's own choice). The group keeps the object's place, id, name,
|
||||
visibility, lock, opacity, blending mode, knockout, opacity mask, attributes, slice and text wrap, and the groups
|
||||
are selected.
|
||||
A layer in `ids` stays a layer: its members are replaced by the edges, which are selected. Objects without a visible 3D effect do not change. It is one undo step and
|
||||
A layer in `ids` stays a layer: its members are replaced by the edges, which are selected, and its ruler guides stay. Objects without a visible 3D effect do not change. It is one undo step and
|
||||
returns `{ids}`, the groups. It is an error, and nothing changes, when nothing is selected or given, when no object
|
||||
has a visible 3D effect, when an object's solid can't be built or the canvas draws its source art in its place, or when one object's wireframe has more than
|
||||
1,000,000 edges.
|
||||
|
||||
Reference in New Issue
Block a user