mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
A live-effect plug-in was given only its object's geometry. An effect
that draws as wide as the stroke it sits on, or tells its fills apart,
had to ask for them again as parameters, which then went stale when
the paint changed.
A live effect's input object now holds the object's fills and strokes
too, read only and in the form a filter gets them (one encoder,
objects::paint_lists, for both), and _context.item says where the
effect sits: "object", or {"fill": i} / {"stroke": i} for an effect in
a fill's or a stroke's own effects. effects::GeomContext carries the
object's appearance and the item, and the plug-in cache keys on the
paints, so a change of paint runs the plug-in again. Hosts from before
this give geometry only; nothing in the output changes.
Expand worked a fill's geometry effects out in the object's context, so
a plug-in effect there would be told "object" and a gradient fill was
expanded into a shape the canvas doesn't draw: fill_shape now gives them
the fill's context, as drawing does (expand.rs).
Siblings named with a declared prefix, the other half of such context,
are left out: the effected geometry is worked out from the object alone
wherever it is drawn, measured, hit-tested or expanded.
wat::when_given is a plug-in whose output depends on whether its input
or parameters hold a text. tests_plugins.rs uses it to check that an
effect sees the stroke's width and the fill, runs again for a thinner
stroke, and on a second stroke is told so; effect.rs checks the item
counting; a gradient fill under such an effect is drawn and expanded
into the same triangle. CRAFT_PLUGIN_PAINTS_WASM=<folder> saves a test
effect to try in the app.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un
173 lines
8.6 KiB
Rust
173 lines
8.6 KiB
Rust
//! Minimal plug-ins in WebAssembly text (WAT): the skeleton `docs/plugins.md` describes, which the
|
|
//! tests turn into modules (with the `wat` crate) to exercise the host.
|
|
|
|
/// Where [`returning`] keeps its output in memory (the manifest sits at 16).
|
|
const OUTPUT_AT: usize = 0x1_0000;
|
|
|
|
/// `s` as the body of a WAT string literal (every byte that isn't a letter, digit or space
|
|
/// escaped).
|
|
pub fn string(s: &str) -> String {
|
|
s.bytes().map(|b| if b.is_ascii_alphanumeric() || b == b' ' { char::from(b).to_string() } else { format!("\\{b:02x}") }).collect()
|
|
}
|
|
|
|
/// A plug-in module: `manifest` (JSON), a `vc_run` whose body is `run` (parameters `$in $il $pp
|
|
/// $pl`, result i64), extra module fields `extra` and `pages` of initial memory. `vc_alloc` grows
|
|
/// memory by whole pages, so its blocks never overlap data segments.
|
|
pub fn module(manifest: &str, run: &str, extra: &str, pages: u32) -> String {
|
|
format!(
|
|
r#"(module
|
|
{extra}
|
|
(memory (export "memory") {pages})
|
|
(data (i32.const 16) "{data}")
|
|
(func (export "vc_abi_version") (result i32) (i32.const 1))
|
|
(func (export "vc_manifest") (result i64) (i64.or (i64.shl (i64.const {len}) (i64.const 32)) (i64.const 16)))
|
|
(func (export "vc_alloc") (param $n i32) (result i32) (local $old i32)
|
|
(local.set $old (memory.grow (i32.shr_u (i32.add (local.get $n) (i32.const 65535)) (i32.const 16))))
|
|
(if (result i32) (i32.eq (local.get $old) (i32.const -1)) (then (i32.const 0)) (else (i32.mul (local.get $old) (i32.const 65536)))))
|
|
(func (export "vc_run") (param $in i32) (param $il i32) (param $pp i32) (param $pl i32) (result i64)
|
|
{run}))"#,
|
|
data = string(manifest),
|
|
len = manifest.len(),
|
|
)
|
|
}
|
|
|
|
/// A `vc_run` body returning the input unchanged.
|
|
pub const ECHO: &str = "(i64.or (i64.shl (i64.extend_i32_u (local.get $il)) (i64.const 32)) (i64.extend_i32_u (local.get $in)))";
|
|
|
|
/// A plug-in that always returns `output` (whatever it is given).
|
|
pub fn returning(manifest: &str, output: &str) -> String {
|
|
let run = format!("(i64.or (i64.shl (i64.const {}) (i64.const 32)) (i64.const {OUTPUT_AT}))", output.len());
|
|
let extra = format!(r#"(data (i32.const {OUTPUT_AT}) "{}")"#, string(output));
|
|
module(manifest, &run, &extra, (OUTPUT_AT + output.len()).div_ceil(0x1_0000) as u32 + 1)
|
|
}
|
|
|
|
/// What [`when_given`] looks through.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum Given {
|
|
/// The input JSON (the objects).
|
|
Input,
|
|
/// The parameters JSON, `_context` included.
|
|
Params,
|
|
}
|
|
|
|
/// Where [`when_given`] keeps the text it looks for.
|
|
const NEEDLE_AT: usize = 0x8000;
|
|
|
|
/// A plug-in that returns `output` when what it is given (`given`) holds the text `needle`, and
|
|
/// its input unchanged otherwise. A live effect made from it draws one shape or the other, so a
|
|
/// test sees from the geometry whether the host handed the plug-in something.
|
|
pub fn when_given(manifest: &str, given: Given, needle: &str, output: &str) -> String {
|
|
let (src, len) = match given {
|
|
Given::Input => ("$in", "$il"),
|
|
Given::Params => ("$pp", "$pl"),
|
|
};
|
|
let n = needle.len();
|
|
// For each place `$i` the needle fits, compare it byte by byte (`$j`).
|
|
let run = format!(
|
|
r#"(local $i i32) (local $j i32)
|
|
(block $found
|
|
(block $none
|
|
(loop $at
|
|
(br_if $none (i32.gt_u (i32.add (local.get $i) (i32.const {n})) (local.get {len})))
|
|
(local.set $j (i32.const 0))
|
|
(block $next
|
|
(loop $cmp
|
|
(br_if $found (i32.eq (local.get $j) (i32.const {n})))
|
|
(br_if $next (i32.ne (i32.load8_u (i32.add (local.get {src}) (i32.add (local.get $i) (local.get $j))))
|
|
(i32.load8_u (i32.add (i32.const {NEEDLE_AT}) (local.get $j)))))
|
|
(local.set $j (i32.add (local.get $j) (i32.const 1)))
|
|
(br $cmp)))
|
|
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
(br $at)))
|
|
(return {ECHO}))
|
|
(i64.or (i64.shl (i64.const {out}) (i64.const 32)) (i64.const {OUTPUT_AT}))"#,
|
|
out = output.len(),
|
|
);
|
|
let extra = format!(r#"(data (i32.const {NEEDLE_AT}) "{}") (data (i32.const {OUTPUT_AT}) "{}")"#, string(needle), string(output));
|
|
module(manifest, &run, &extra, (OUTPUT_AT + output.len()).div_ceil(0x1_0000) as u32 + 1)
|
|
}
|
|
|
|
/// The manifest of a test plug-in `id` of `kind` with `params` (a JSON object of specs).
|
|
pub fn manifest(id: &str, kind: &str, params: &str) -> String {
|
|
format!(r#"{{"id":"{id}","name":"Test {id}","version":"1","kind":"{kind}","params":{params}}}"#)
|
|
}
|
|
|
|
/// The manifest of a test exporter `id` writing `extensions` (a JSON list) with `params`.
|
|
pub fn exporter(id: &str, extensions: &str, params: &str) -> String {
|
|
format!(r#"{{"id":"{id}","name":"Test {id}","version":"1","kind":"exporter","extensions":{extensions},"params":{params}}}"#)
|
|
}
|
|
|
|
/// What [`echo_exporter`] writes as its file.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum Echo {
|
|
/// The input: the document, as the host hands it over.
|
|
Input,
|
|
/// The parameters JSON, `_context` included.
|
|
Params,
|
|
}
|
|
|
|
/// Where [`echo_exporter`] keeps the base64 alphabet, then its output's opening and closing text.
|
|
const ALPHABET_AT: usize = 0x8000;
|
|
|
|
/// An exporter (manifest `manifest`) whose file is exactly what it is given (`echo`), so a test
|
|
/// reads back what the host hands a plug-in: it returns `{"dataBase64": "…"}`, encoding the bytes
|
|
/// in base64 itself, three bytes to four characters, the last group padded with `=`.
|
|
pub fn echo_exporter(manifest: &str, echo: Echo) -> String {
|
|
let (src, len) = match echo {
|
|
Echo::Input => ("$in", "$il"),
|
|
Echo::Params => ("$pp", "$pl"),
|
|
};
|
|
let (open, close) = (r#"{"dataBase64":""#, r#""}"#);
|
|
let (open_at, close_at) = (ALPHABET_AT + 64, ALPHABET_AT + 96);
|
|
// One character of the alphabet: the six bits of `$b` from `shift` up.
|
|
let char_at = |shift: u32| {
|
|
format!("(i32.load8_u (i32.add (i32.const {ALPHABET_AT}) (i32.and (i32.shr_u (local.get $b) (i32.const {shift})) (i32.const 63))))")
|
|
};
|
|
let byte = |k: u32| format!("(i32.load8_u (i32.add (local.get {src}) (i32.add (local.get $i) (i32.const {k}))))");
|
|
let run = format!(
|
|
r#"(local $o i32) (local $n i32) (local $w i32) (local $i i32) (local $b i32) (local $rest i32)
|
|
(local.set $n (i32.add (i32.const {frame}) (i32.mul (i32.const 4) (i32.div_u (i32.add (local.get {len}) (i32.const 2)) (i32.const 3)))))
|
|
(local.set $o (memory.grow (i32.add (i32.shr_u (local.get $n) (i32.const 16)) (i32.const 1))))
|
|
(if (i32.eq (local.get $o) (i32.const -1)) (then (return (i64.const -1))))
|
|
(local.set $o (i32.mul (local.get $o) (i32.const 65536)))
|
|
(memory.copy (local.get $o) (i32.const {open_at}) (i32.const {open_len}))
|
|
(local.set $w (i32.add (local.get $o) (i32.const {open_len})))
|
|
(block $done (loop $triple
|
|
(br_if $done (i32.gt_u (i32.add (local.get $i) (i32.const 3)) (local.get {len})))
|
|
(local.set $b (i32.or (i32.or (i32.shl {b0} (i32.const 16)) (i32.shl {b1} (i32.const 8))) {b2}))
|
|
(i32.store8 (local.get $w) {c18}) (i32.store8 offset=1 (local.get $w) {c12})
|
|
(i32.store8 offset=2 (local.get $w) {c6}) (i32.store8 offset=3 (local.get $w) {c0})
|
|
(local.set $w (i32.add (local.get $w) (i32.const 4)))
|
|
(local.set $i (i32.add (local.get $i) (i32.const 3)))
|
|
(br $triple)))
|
|
(local.set $rest (i32.sub (local.get {len}) (local.get $i)))
|
|
(if (i32.ne (local.get $rest) (i32.const 0)) (then
|
|
(local.set $b (i32.shl {b0} (i32.const 16)))
|
|
(if (i32.eq (local.get $rest) (i32.const 2)) (then (local.set $b (i32.or (local.get $b) (i32.shl {b1} (i32.const 8))))))
|
|
(i32.store8 (local.get $w) {c18}) (i32.store8 offset=1 (local.get $w) {c12})
|
|
(i32.store8 offset=2 (local.get $w) (if (result i32) (i32.eq (local.get $rest) (i32.const 2)) (then {c6}) (else (i32.const 61))))
|
|
(i32.store8 offset=3 (local.get $w) (i32.const 61))
|
|
(local.set $w (i32.add (local.get $w) (i32.const 4)))))
|
|
(memory.copy (local.get $w) (i32.const {close_at}) (i32.const {close_len}))
|
|
(i64.or (i64.shl (i64.extend_i32_u (local.get $n)) (i64.const 32)) (i64.extend_i32_u (local.get $o)))"#,
|
|
frame = open.len() + close.len(),
|
|
open_len = open.len(),
|
|
close_len = close.len(),
|
|
b0 = byte(0),
|
|
b1 = byte(1),
|
|
b2 = byte(2),
|
|
c18 = char_at(18),
|
|
c12 = char_at(12),
|
|
c6 = char_at(6),
|
|
c0 = char_at(0),
|
|
);
|
|
let extra = format!(
|
|
r#"(data (i32.const {ALPHABET_AT}) "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
|
|
(data (i32.const {open_at}) "{}")
|
|
(data (i32.const {close_at}) "{}")"#,
|
|
string(open),
|
|
string(close)
|
|
);
|
|
module(manifest, &run, &extra, 1)
|
|
}
|