mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 05:24:44 +00:00
Effects: a plug-in before a fill's or stroke's own 3D effect is told its fill or stroke
A 3D effect on a fill or stroke worked on copies of the object's fills
and strokes, and the copies were looked up among the object's items by
identity, so a live-effect plug-in before that 3D effect was given
`_context.item` "object" and a deferred stage there was listed with no
item. The drawn art, the 3D meshes and Expand as Wireframes now pass the
original item, so the plug-in is given `{"fill": i}` or `{"stroke": i}`
as docs/plugins.md describes. The paints of a type's first run still have
no item.
This commit is contained in:
@@ -209,6 +209,19 @@ pub(crate) fn solid_paints(n: &Node) -> Vec<AppearanceItem> {
|
||||
runs
|
||||
}
|
||||
|
||||
/// [`solid_paints`] with, for each paint, the item of `n` it copies: the item a live-effect plug-in
|
||||
/// before the paint's own 3D effect is told it sits on. The paints of a type's first run copy no
|
||||
/// item of `n` and have none.
|
||||
pub(crate) fn solid_paint_sources(n: &Node) -> (Vec<Option<&AppearanceItem>>, Vec<AppearanceItem>) {
|
||||
let own: Vec<&AppearanceItem> = n.appearance.items.iter().filter(|i| crate::paints(i)).collect();
|
||||
if own.is_empty() {
|
||||
let paints = solid_paints(n);
|
||||
return (vec![None; paints.len()], paints);
|
||||
}
|
||||
let paints = own.iter().map(|i| (*i).clone()).collect();
|
||||
(own.into_iter().map(Some).collect(), paints)
|
||||
}
|
||||
|
||||
fn facets(id: &str, path: &PathData, rule: FillRule, bounds: Rect, p: &Value) -> Result<Vec<Facet>, String> {
|
||||
let view = view_options(p);
|
||||
match id {
|
||||
@@ -1771,7 +1784,7 @@ pub fn solid_art_in(doc: Option<&Document>, n: &Node) -> Option<Node> {
|
||||
}
|
||||
let (path, rule) = profile(n)?;
|
||||
let ctx = GeomContext::of(n);
|
||||
let paints = solid_paints(n);
|
||||
let (sources, paints) = solid_paint_sources(n);
|
||||
let mut children = vec![];
|
||||
let remaining: Vec<Effect>;
|
||||
if let Some((index, fx)) = solid(&n.appearance.effects) {
|
||||
@@ -1789,9 +1802,9 @@ pub fn solid_art_in(doc: Option<&Document>, n: &Node) -> Option<Node> {
|
||||
crate::raster::follow_paints(&mut paints, &n.appearance.effects);
|
||||
// Every item's shadow goes behind every item's art.
|
||||
let mut shadows = vec![];
|
||||
for item in &paints {
|
||||
for (source, item) in sources.into_iter().zip(&paints) {
|
||||
if let Some((index, fx)) = solid(item.effects()) {
|
||||
let p = geometry(&path, item.effects().get(..index)?, &ctx.item(item));
|
||||
let p = geometry(&path, item.effects().get(..index)?, &ctx.item(source.unwrap_or(item)));
|
||||
let (shadow, surfaces) = art_for(doc, n, fx, &[Profile { path: p, rule, items: vec![item.clone()], opacity: 1.0, scopes: vec![] }])?;
|
||||
let later: Vec<Effect> = item.effects().iter().skip(index + 1).filter(|e| !is_three_d(&e.id)).cloned().collect();
|
||||
let with_later = |mut art: Arc<Node>| {
|
||||
@@ -2086,16 +2099,16 @@ fn meshes_within(doc: Option<&Document>, n: &Node, total: &mut usize, limit: usi
|
||||
} else {
|
||||
let (path, rule) = object_profile(n, label)?;
|
||||
let ctx = GeomContext::of(n);
|
||||
let paints = solid_paints(n);
|
||||
let (sources, paints) = solid_paint_sources(n);
|
||||
if let Some((index, fx)) = solid(&n.appearance.effects) {
|
||||
let path = geometry(&path, n.appearance.effects.get(..index).unwrap_or_default(), &ctx);
|
||||
meshes_for(fx, &[Profile { path, rule, items: paints, opacity: 1.0, scopes: vec![] }], total, limit)?
|
||||
} else {
|
||||
let path = geometry(&path, &n.appearance.effects, &ctx);
|
||||
let mut out = vec![];
|
||||
for item in &paints {
|
||||
for (source, item) in sources.into_iter().zip(&paints) {
|
||||
if let Some((index, fx)) = solid(item.effects()) {
|
||||
let p = geometry(&path, item.effects().get(..index).unwrap_or_default(), &ctx.item(item));
|
||||
let p = geometry(&path, item.effects().get(..index).unwrap_or_default(), &ctx.item(source.unwrap_or(item)));
|
||||
out.extend(meshes_for(fx, &[Profile { path: p, rule, items: vec![item.clone()], opacity: 1.0, scopes: vec![] }], total, limit)?);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use vectorcraft_three_d::Facet;
|
||||
|
||||
use super::{
|
||||
Profile, ROTATE, built_solids, facets, geometry, has_solid, is_container, is_plane, is_three_d, label_of, members, object_profile, region,
|
||||
reshape_members, resolve, resolve_for, solid, solid_paints, solid_parts, union_bounds,
|
||||
reshape_members, resolve, resolve_for, solid, solid_paint_sources, solid_parts, union_bounds,
|
||||
};
|
||||
use crate::{GeomContext, is_geometry};
|
||||
|
||||
@@ -105,16 +105,16 @@ fn solid_face_paths(doc: Option<&Document>, n: &Node) -> Result<Vec<PathData>, S
|
||||
let label = n.appearance.effects.iter().find(|e| e.visible && is_three_d(&e.id)).map_or("This effect", |e| label_of(&e.id));
|
||||
let (path, rule) = object_profile(n, label)?;
|
||||
let ctx = GeomContext::of(n);
|
||||
let paints = solid_paints(n);
|
||||
let (sources, paints) = solid_paint_sources(n);
|
||||
if let Some((index, fx)) = solid(&n.appearance.effects) {
|
||||
let path = geometry(&path, n.appearance.effects.get(..index).unwrap_or_default(), &ctx);
|
||||
return facet_paths(fx, &[Profile { path, rule, items: paints, opacity: 1.0, scopes: vec![] }]);
|
||||
}
|
||||
let path = geometry(&path, &n.appearance.effects, &ctx);
|
||||
let mut out = vec![];
|
||||
for item in &paints {
|
||||
for (source, item) in sources.into_iter().zip(&paints) {
|
||||
if let Some((index, fx)) = solid(item.effects()) {
|
||||
let p = geometry(&path, item.effects().get(..index).unwrap_or_default(), &ctx.item(item));
|
||||
let p = geometry(&path, item.effects().get(..index).unwrap_or_default(), &ctx.item(source.unwrap_or(item)));
|
||||
let faces = facet_paths(fx, &[Profile { path: p, rule, items: vec![item.clone()], opacity: 1.0, scopes: vec![] }])?;
|
||||
out.extend(reshaped(faces, item.effects().get(index + 1..).unwrap_or_default(), &GeomContext { doc, ..ctx }));
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ mod imageoutline;
|
||||
mod painted;
|
||||
mod rasterfollow;
|
||||
mod scale;
|
||||
mod solidcontext;
|
||||
mod transformfx;
|
||||
|
||||
fn fx(id: &str, params: Value) -> Effect {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
//! A live-effect plug-in before a fill's or stroke's own 3D effect is told which fill or stroke it
|
||||
//! is on, in the drawn art, the exported meshes and the wireframe.
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, Effect, FillLayer, Node, NodeId};
|
||||
use vectorcraft_geom::{PathData, Rect, shapes};
|
||||
use vectorcraft_plugins::wat as tpl;
|
||||
|
||||
use crate::{EXTRUDE, solid_art, three_d_meshes_in, three_d_wireframe_in};
|
||||
|
||||
/// Widths the plug-in's squares are drawn at: `LARGE` when it is told it is on the item it looks
|
||||
/// for, `SMALL` otherwise.
|
||||
const LARGE: f64 = 600.0;
|
||||
const SMALL: f64 = 6.0;
|
||||
|
||||
/// A rectangle with a fill, a 1 pt stroke and a second fill on top. The item at `at` (an index in
|
||||
/// its appearance items) has, in its own effects, live-effect plug-in `name` and then Extrude &
|
||||
/// Bevel. The plug-in returns a `LARGE` square when it is given `item`, and a `SMALL` one otherwise.
|
||||
fn plugin_then_extrude(name: &str, at: usize, item: &str) -> Node {
|
||||
let square = |w: f64| shapes::rectangle(Rect::new(0.0, 0.0, w, w));
|
||||
let id = format!("test.effects.solidcontext.{name}");
|
||||
let manifest = format!(r#"{{"id":"{id}","name":"Test {id}","version":"1","kind":"effect","params":{{}}}}"#);
|
||||
let (large, small) = (json!({"objects": [{"path": square(LARGE)}]}).to_string(), json!({"objects": [{"path": square(SMALL)}]}).to_string());
|
||||
let module = wat::parse_str(tpl::when_given_else(&manifest, tpl::Given::Params, item, &large, &small)).unwrap();
|
||||
vectorcraft_plugins::registry::install_bytes(&module).unwrap();
|
||||
let mut appearance = Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::solid(Color::BLACK), 1.0);
|
||||
appearance.items.push(AppearanceItem::Fill(FillLayer::new(Paint::solid(Color::rgb(0.0, 0.0, 1.0)))));
|
||||
let effects = appearance.items[at].effects_mut();
|
||||
effects.push(Effect { id: format!("plugin.{id}"), params: json!({}), visible: true });
|
||||
effects.push(crate::new_effect(EXTRUDE, &json!({})).unwrap());
|
||||
Node::path(NodeId(7), shapes::rectangle(Rect::new(0.0, 0.0, 20.0, 20.0)), appearance)
|
||||
}
|
||||
|
||||
/// The width of the union of `paths`' bounds.
|
||||
fn width(paths: impl IntoIterator<Item = PathData>) -> f64 {
|
||||
paths.into_iter().filter_map(|p| p.bounds()).reduce(|a, b| a.union(b)).map_or(0.0, |b| b.width())
|
||||
}
|
||||
|
||||
/// Whether each of the art, the meshes and the wireframe `n`'s 3D effect draws is wider than half
|
||||
/// of `LARGE`, with the widths.
|
||||
fn drawn_large(n: &Node) -> ([bool; 3], [f64; 3]) {
|
||||
let art = solid_art(n).unwrap();
|
||||
let mut paths = vec![];
|
||||
art.walk(&mut |c| paths.extend(c.path_data().cloned()));
|
||||
let meshes = three_d_meshes_in(None, n).unwrap();
|
||||
let xs = meshes.iter().flat_map(|m| m.mesh.positions.iter().map(|p| p[0]));
|
||||
let mesh_width = xs.clone().fold(f64::NEG_INFINITY, f64::max) - xs.fold(f64::INFINITY, f64::min);
|
||||
let widths = [width(paths), mesh_width, width(three_d_wireframe_in(None, n).unwrap())];
|
||||
(widths.map(|w| w > LARGE / 2.0), widths)
|
||||
}
|
||||
|
||||
/// A plug-in before the second fill's Extrude & Bevel is given `{"fill": 1}`, not `"object"`, so
|
||||
/// the art, meshes and wireframe are all built from the square it returns for that fill.
|
||||
#[test]
|
||||
fn a_plugin_before_a_fills_extrude_is_told_its_fill() {
|
||||
let (large, widths) = drawn_large(&plugin_then_extrude("fill", 2, r#""item":{"fill":1}"#));
|
||||
assert_eq!(large, [true; 3], "the art, meshes and wireframe are {widths:?} wide");
|
||||
}
|
||||
|
||||
/// A plug-in before the stroke's Extrude & Bevel is given `{"stroke": 0}`.
|
||||
#[test]
|
||||
fn a_plugin_before_a_strokes_extrude_is_told_its_stroke() {
|
||||
let (large, widths) = drawn_large(&plugin_then_extrude("stroke", 1, r#""item":{"stroke":0}"#));
|
||||
assert_eq!(large, [true; 3], "the art, meshes and wireframe are {widths:?} wide");
|
||||
}
|
||||
|
||||
/// A plug-in on a fill is not told it is on the object: the square it returns otherwise is the one
|
||||
/// extruded.
|
||||
#[test]
|
||||
fn a_plugin_on_a_fill_is_not_told_it_is_on_the_object() {
|
||||
let (large, widths) = drawn_large(&plugin_then_extrude("object", 2, r#""item":"object""#));
|
||||
assert_eq!(large, [false; 3], "the art, meshes and wireframe are {widths:?} wide");
|
||||
}
|
||||
@@ -1873,6 +1873,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
|
||||
- **Plug-in parameters as their manifest presents them (G17):** each parameter may have a `label`, a `unit` shown after its value, `help` on its label and a `helpUrl` page opened from a button, a `group` (sections that fold, after the parameters outside any group) and a `visibleWhen` that shows it only while the checkboxes and choices it names have one of the listed values (a hidden parameter still goes to the plug-in). `string` and `numberList` parameters join number, int, bool and choice, and a plug-in declares up to 256 parameters, at most 64 outside groups. The filter, live-effect and exporter dialogs use these keys, and the Appearance panel's effect editor the labels, units, help and conditions; hosts from before them ignore them. Regressions: `crates/plugins/src/dialog.rs`, `crates/plugins/src/manifest.rs`, `crates/ui-egui/src/dialogs/tests_plugin_fields.rs`, `crates/ui-egui/src/panels/appearance.rs`. Scores and hours unchanged.
|
||||
- **Live-effect plug-ins read their object's paints (G17):** a live effect is given the object's `fills` and `strokes` as a filter gets them, read only, and `_context.item` (`"object"`, or the fill or stroke whose own effects hold it); the result is cached by paint too, so a change of paint runs it again. Regressions: `crates/engine/src/tests/plugins.rs`, `crates/plugins/src/effect.rs`. Scores and hours unchanged.
|
||||
- **Live-effect plug-ins before a fill's or stroke's own 3D effect:** a live effect listed before a fill's or stroke's own Extrude & Bevel, Inflate or Rotate effect is given `_context.item` for that fill or stroke (`{"fill": i}` or `{"stroke": i}`), not `"object"`, in the drawn art, in the 3D meshes and in Expand as Wireframes; a deferred stage there is listed at that fill's or stroke's place. Regressions: `crates/effects/src/tests/solidcontext.rs`. Scores and hours unchanged.
|
||||
- **Filter plug-ins set effects and stroke styles (G17):** a filter is given each object's effect records and its strokes' `join`, `cap` and `miterLimit`, and may return them; the records are admitted as `effect.apply` admits them (a known effect or installed live-effect plug-in, parameters it takes, one 3D effect that fits, no texture read from a file), and one refused refuses the output. Regressions: `crates/engine/src/tests/plugins.rs`, `crates/plugins/src/objects/tests.rs`. Scores and hours unchanged.
|
||||
- **Gradient stop plates from filter plug-ins are clamped (G17):** the four process `plates` of a gradient stop in a filter's or importer's output are clamped to 0–1, as its other colors are; a value that is not a number becomes 0, and an infinite one becomes 0 or 1 by its sign. Painted live effects still refuse such a stop. Regressions: `crates/plugins/src/objects/tests.rs`. Scores and hours unchanged.
|
||||
- **Plug-in data kept with the document (G17):** a filter writes JSON of at most 1 MiB with `documentData` (one undo step with its objects; `null` removes it) and reads it back in `_context.documentData`, which no other filter is given. It is the document's foreign data (`unknown.pluginData`, by plug-in id), as the perspective grid is, so the native file keeps it with no section of its own; an exporter is given the whole document, so it can read every plug-in's. Regressions: `crates/engine/src/tests/plugins.rs`, `crates/engine/tests/plugin_export.rs` (the exporter), `crates/doc/src/plugin_data.rs`, `crates/plugins/src/objects/tests.rs`. Scores and hours unchanged.
|
||||
|
||||
Reference in New Issue
Block a user