mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
Merge remote-tracking branch 'origin/main' into HEAD
This commit is contained in:
+10
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~30 points of "ready for real work" and ~350–620 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-22 · **Last updated:** 2026-10-30 · **Change:** minor (localization: 303 more panel/dialog rows in Japanese and the Chinese catalogs; recounted after #1774: Japanese 27% → 46%, Simplified Chinese 29% → 46%, Traditional Chinese 29% → 38%, overall 18% → 24%) · **Target:** Adobe After Effects 2026 (26.5.0.89)
|
||||
> **Last reviewed:** 2026-10-22 · **Last updated:** 2026-10-30 · **Change:** minor (features of 30 October in the progress log: text Line Join, Timeline drag-reorder and multi-select of effects, viewer edge pan, ScriptUI drag capture and dock size; previously: localization recount after #1774) · **Target:** Adobe After Effects 2026 (26.5.0.89)
|
||||
|
||||
EffectCraft aims to do what After Effects does, with the same panels, menus and behaviour, written
|
||||
from scratch in Rust. This page is the one-page summary; the evidence is in
|
||||
@@ -117,6 +117,14 @@ Come tell us what matters most to you on [Discord](https://discord.gg/artcraft).
|
||||
|
||||
## Progress log
|
||||
|
||||
- 2026-10-30: Text strokes take a Line Join (Miter, Round or Bevel) in the Character panel,
|
||||
Miter for new text as in After Effects (#2077). In the Timeline, effects, masks and shape items
|
||||
drag to a new place in their stack (#2210) and Shift-click adds them to the selection, so one
|
||||
animation preset can hold several (#2552). The Composition panel pans while a mask vertex is
|
||||
held at its edge (#999). Effect buffers that pad a small layer stay within a memory budget
|
||||
(#2261); an MP4 lasts as long as its edit list shows (#2592); the Render Queue's times tick
|
||||
between frames (#2593). ScriptUI drags keep their control past the window edge with
|
||||
`MouseEvent.buttons` (#2584), and docked panels get their dock's size and `onResize` (#2585).
|
||||
- 2026-10-28: A shape layer's Rectangle and Ellipse Size get the Constrain Proportions chain
|
||||
that Scale has, so a circle stays a circle while its size is typed, scrubbed or keyed; it
|
||||
starts off, so existing shapes behave as before (#2489).
|
||||
@@ -169,6 +177,7 @@ Come tell us what matters most to you on [Discord](https://discord.gg/artcraft).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | Progress log: text Line Join, Timeline drag-reorder and Shift multi-select of effects, viewer edge pan, effect padding budget, MP4 edit-list duration, Render Queue clock, ScriptUI drag capture and docked-panel size |
|
||||
| 2026-10-22 | minor | Localization: 300 more panel/dialog rows in Japanese and the Chinese catalogs, plus the review's three missing rows (Basic, Advanced, Play/Stop) — panel tabs, form and dialog labels, context menus, tracker/graph/text-panel controls, File-panel footers, credits; language shares recounted after #1774 (Japanese 27% → 46% with 297 effect names, Simplified Chinese 29% → 46% with 307 effect names, Traditional Chinese 29% → 38%, overall 18% → 24%) |
|
||||
| 2026-10-12 | minor | Open-issue count (134 to 216), crash list and localization share (12% to 18%) updated to match docs/gaps.md |
|
||||
| 2026-10-12 | patch | Roto Brush span validation after trimming; command regressions for both layer edges |
|
||||
|
||||
@@ -581,6 +581,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
always,
|
||||
scriptui::mouse
|
||||
),
|
||||
cmd!(
|
||||
"scriptui.resize",
|
||||
"Resize Script Window",
|
||||
[],
|
||||
None,
|
||||
"{window?, size: [width, height] (pixels)} → the window takes the size, lays itself out again and its onResizing / onResize run (a docked ScriptUI panel gets its dock's size)",
|
||||
always,
|
||||
scriptui::resize
|
||||
),
|
||||
cmd!(
|
||||
"scriptui.close",
|
||||
"Close Script Window",
|
||||
|
||||
@@ -769,6 +769,17 @@ pub(crate) fn mouse(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
dispatch(s, ScriptUiEvent { window, widget, kind: (*ty).to_string(), value })
|
||||
}
|
||||
|
||||
/// `scriptui.resize`: the host gave the window a new size (a docked panel's dock, #2585): the
|
||||
/// script's window takes it, lays itself out again and gets `onResizing` / `onResize`.
|
||||
pub(crate) fn resize(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let c = "scriptui.resize";
|
||||
let window = window_p(s, p, c)?;
|
||||
let size = p.get("size").and_then(Value::as_array).ok_or_else(|| crate::commands::bad(c, "missing `size` [width, height]"))?;
|
||||
let side = |i: usize| size.get(i).and_then(Value::as_f64).filter(|v| v.is_finite()).map(|v| v.round().clamp(1.0, 16384.0));
|
||||
let (Some(w), Some(h)) = (side(0), side(1)) else { return Err(crate::commands::bad(c, "`size` must be [width, height] in pixels")) };
|
||||
dispatch(s, ScriptUiEvent { window, widget: 0, kind: "resize".into(), value: json!([w, h]) })
|
||||
}
|
||||
|
||||
/// `scriptui.close`: close a window (a dialog's `show()` returns `result`, default 2 = Cancel).
|
||||
pub(crate) fn close(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let window = window_p(s, p, "scriptui.close")?;
|
||||
|
||||
@@ -431,12 +431,13 @@ impl<'a> Cx<'a> {
|
||||
fn crop_resize(&self, img: Image) -> Image {
|
||||
let om = self.job.output;
|
||||
let scale = self.comp().map(|c| self.job.settings.effective_scale(c)).unwrap_or_else(|| self.job.settings.resolution.clamp(0.01, 4.0));
|
||||
// frame_size applies crop itself: use the original frame and the same scale as edges.
|
||||
let (w, h) = om.frame_size(img.width, img.height, scale);
|
||||
let (t, l, b, r) = om.crop.edges(img.width, img.height, scale);
|
||||
let img = if (t, l, b, r) == (0, 0, 0, 0) { img } else { crop(&img, t, l, b, r) };
|
||||
if !om.resize.enabled {
|
||||
return img;
|
||||
}
|
||||
let (w, h) = om.frame_size(img.width, img.height, 1.0);
|
||||
if (w, h) == (img.width, img.height) {
|
||||
return img;
|
||||
}
|
||||
@@ -1062,6 +1063,91 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Regress the locked-aspect example: cropping 1920x1080 must resize to 1280x1440.
|
||||
#[test]
|
||||
fn crop_resize_applies_margins_once() {
|
||||
use effectcraft_project::render_queue::{Crop, OutputModule, Resize};
|
||||
let output = OutputModule {
|
||||
crop: Crop { enabled: true, left: 480, right: 480, ..Default::default() },
|
||||
resize: Resize { enabled: true, width: 1280, lock_aspect: true, quality: ResizeQuality::Low, ..Default::default() },
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(output.frame_size(1920, 1080, 1.0), (1280, 1440));
|
||||
let img = run_crop_resize(Image::new(1920, 1080), &output, 1.0);
|
||||
assert_eq!((img.width, img.height), (1280, 1440));
|
||||
assert_eq!(img.data.len(), 1280 * 1440);
|
||||
}
|
||||
|
||||
/// Apply output-module geometry to a synthetic frame without rendering or writing files.
|
||||
fn run_crop_resize(img: Image, output: &effectcraft_project::render_queue::OutputModule, scale: f64) -> Image {
|
||||
use effectcraft_project::render_queue::RenderSettings;
|
||||
let project = Project::default();
|
||||
let settings = RenderSettings { resolution: scale, ..Default::default() };
|
||||
let job = Job {
|
||||
project: &project,
|
||||
footage: &effectcraft_render::NoFootage,
|
||||
expr: None,
|
||||
accel: None,
|
||||
comp: effectcraft_project::ItemId(0),
|
||||
settings: &settings,
|
||||
output,
|
||||
path: "unused.png",
|
||||
sink: None,
|
||||
nested_switches: true,
|
||||
options: Default::default(),
|
||||
};
|
||||
Cx::new(&job).crop_resize(img)
|
||||
}
|
||||
|
||||
/// Match predicted dimensions across crop, padding, ROI scale, aspect locks and resize qualities.
|
||||
#[test]
|
||||
fn crop_resize_dimensions_match_the_output_module() {
|
||||
use effectcraft_project::render_queue::{Crop, OutputModule, Resize};
|
||||
let edges = Crop { enabled: true, left: 2, right: 2, ..Default::default() };
|
||||
let padding = Crop { enabled: true, left: -2, bottom: -2, ..Default::default() };
|
||||
let roi = |rect| Crop { enabled: true, use_roi: true, roi: Some(rect), ..Default::default() };
|
||||
for quality in [ResizeQuality::Low, ResizeQuality::High] {
|
||||
let resize = |width| Resize { enabled: true, width, height: 1, lock_aspect: true, quality };
|
||||
for (crop, resize, scale, expected) in [
|
||||
(Crop::default(), Resize::default(), 1.0, (8, 6)),
|
||||
(edges, Resize::default(), 1.0, (4, 6)),
|
||||
(Crop::default(), resize(4), 1.0, (4, 3)),
|
||||
(edges, resize(8), 1.0, (8, 12)),
|
||||
(padding, resize(5), 1.0, (5, 4)),
|
||||
(roi([4.0, 2.0, 8.0, 6.0]), resize(8), 0.5, (8, 6)),
|
||||
(roi([1.0, 0.5, 2.0, 1.5]), resize(8), 2.0, (8, 6)),
|
||||
(edges, Resize { lock_aspect: false, height: 2, ..resize(5) }, 1.0, (5, 2)),
|
||||
] {
|
||||
let output = OutputModule { crop, resize, ..Default::default() };
|
||||
assert_eq!(output.frame_size(8, 6, scale), expected);
|
||||
let img = run_crop_resize(Image::filled(8, 6, [0.25, 0.0, 0.0, 0.5]), &output, scale);
|
||||
assert_eq!((img.width, img.height), expected, "{crop:?}, {resize:?}, scale {scale}");
|
||||
assert_eq!(img.data.len(), expected.0 as usize * expected.1 as usize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Exercise zero/empty inputs, collapsed crops and existing caps on extreme single-axis resizes.
|
||||
#[test]
|
||||
fn crop_resize_zero_and_extreme_sizes_stay_bounded() {
|
||||
use effectcraft_project::render_queue::{Crop, OutputModule, Resize};
|
||||
for quality in [ResizeQuality::Low, ResizeQuality::High] {
|
||||
for (size, crop, width, height, expected) in [
|
||||
((8, 6), Crop::default(), 0, 0, (1, 1)),
|
||||
((8, 1), Crop::default(), u32::MAX, 1, (30_000, 1)),
|
||||
((1, 8), Crop::default(), 1, u32::MAX, (1, 30_000)),
|
||||
((8, 6), Crop { enabled: true, left: 20_000, right: 20_000, ..Default::default() }, 2, 3, (2, 3)),
|
||||
((0, 0), Crop { enabled: true, left: 1, top: 1, ..Default::default() }, 2, 3, (2, 3)),
|
||||
] {
|
||||
let output = OutputModule { crop, resize: Resize { enabled: true, width, height, lock_aspect: false, quality }, ..Default::default() };
|
||||
let img = run_crop_resize(Image::new(size.0, size.1), &output, 1.0);
|
||||
assert_eq!((img.width, img.height), expected);
|
||||
assert_eq!(img.data.len(), expected.0 as usize * expected.1 as usize);
|
||||
assert!(img.data.iter().all(|p| *p == [0.0; 4]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crop_and_pad() {
|
||||
let mut img = Image::new(4, 3);
|
||||
|
||||
@@ -174,6 +174,31 @@ fn crop_roi_and_resize() {
|
||||
assert_eq!((z.width(), z.height()), (32, 24));
|
||||
}
|
||||
|
||||
/// Resizing to the cropped frame's own dimensions must preserve every decoded PNG pixel.
|
||||
/// Compare against crop-only output for padding and scaled ROIs with both resize qualities.
|
||||
#[test]
|
||||
fn crop_resize_preserves_pixels_when_the_cropped_size_is_requested() {
|
||||
let (p, cid) = project(FrameRate::new(25, 1), |_, _| {});
|
||||
for (crop, scale, expected) in [
|
||||
(Crop { enabled: true, left: 16, right: 16, ..Default::default() }, 1.0, (32, 48)),
|
||||
(Crop { enabled: true, left: -4, top: -2, ..Default::default() }, 1.0, (68, 50)),
|
||||
(Crop { enabled: true, use_roi: true, roi: Some([0.0, 24.0, 32.0, 24.0]), ..Default::default() }, 0.5, (16, 12)),
|
||||
(Crop { enabled: true, use_roi: true, roi: Some([0.0, 24.0, 32.0, 24.0]), ..Default::default() }, 2.0, (64, 48)),
|
||||
] {
|
||||
let settings = RenderSettings { resolution: scale, ..custom(0.4, 0.44) };
|
||||
let mut output = OutputModule { crop, ..OutputModule::for_format(OutputFormat::PngSequence) };
|
||||
let reference = png(&run(&p, cid, &settings, &output, "/c/reference_[#####].png"), 0);
|
||||
assert_eq!(reference.dimensions(), expected);
|
||||
assert!(reference.pixels().any(|p| p.0[0] > 150), "the reference contains visible content");
|
||||
for quality in [ResizeQuality::Low, ResizeQuality::High] {
|
||||
output.resize = Resize { enabled: true, width: expected.0, height: 1, lock_aspect: true, quality };
|
||||
let actual = png(&run(&p, cid, &settings, &output, "/c/resized_[#####].png"), 0);
|
||||
assert_eq!(actual.dimensions(), expected);
|
||||
assert!(actual == reference, "crop {crop:?}, scale {scale}, quality {quality:?}: pixels changed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alpha_only_and_premultiplied() {
|
||||
let (p, cid) = project(FrameRate::new(25, 1), |p, comp| {
|
||||
|
||||
@@ -912,6 +912,18 @@ function __uiDispatch(winId, id, kind, valueJson) {
|
||||
w.close(v === null ? 2 : v);
|
||||
return;
|
||||
}
|
||||
// The host gave the window a new size (a docked panel's dock): take it, lay out, tell the script.
|
||||
if (kind === "resize") {
|
||||
var nw = Number(v && v[0]), nh = Number(v && v[1]);
|
||||
var b = w.__ub || w.__lb;
|
||||
if (nw > 0 && nh > 0 && (b[2] - b[0] !== nw || b[3] - b[1] !== nh)) {
|
||||
w.__ub = [b[0], b[1], b[0] + nw, b[1] + nh];
|
||||
__uiLayout(w);
|
||||
w.__fire("onResizing");
|
||||
w.__fire("onResize");
|
||||
}
|
||||
return;
|
||||
}
|
||||
var c = __uiFind(w, id);
|
||||
if (!c || c.enabled === false) return;
|
||||
var radio = function () {
|
||||
|
||||
@@ -26,6 +26,8 @@ enum Act {
|
||||
Close(u32),
|
||||
/// A mouse event (`scriptui.mouse` parameters without window and widget).
|
||||
Mouse(u32, u32, Value),
|
||||
/// A docked panel's new size (`scriptui.resize`).
|
||||
Resize(u32, [f32; 2]),
|
||||
}
|
||||
|
||||
fn rect_of(origin: egui::Pos2, b: [f64; 4]) -> Rect {
|
||||
@@ -500,6 +502,7 @@ fn perform(app: &mut EffectcraftApp, ctx: &egui::Context, acts: Vec<Act>) {
|
||||
Act::Set(w, id, v) => ("scriptui.set", json!({"window": w, "widget": id, "value": v})),
|
||||
Act::Changing(w, id, v) => ("scriptui.set", json!({"window": w, "widget": id, "value": v, "changing": true})),
|
||||
Act::Close(w) => ("scriptui.close", json!({"window": w})),
|
||||
Act::Resize(w, [x, y]) => ("scriptui.resize", json!({"window": w, "size": [x, y]})),
|
||||
Act::Mouse(w, id, mut v) => {
|
||||
if let Some(o) = v.as_object_mut() {
|
||||
o.insert("window".into(), json!(w));
|
||||
@@ -582,6 +585,14 @@ pub fn panel(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect, id: u32) {
|
||||
// A modal script dialog blocks the panels too.
|
||||
let modal = app.session.script_ui.windows.iter().any(|w| w.modal);
|
||||
let mut acts = vec![];
|
||||
// The script learns its dock's size when it changes (#2585), as After Effects tells a
|
||||
// docked panel: its window takes the size and onResize runs.
|
||||
let dock = [rect.width().round(), rect.height().round()];
|
||||
let told = egui::Id::unique(("scriptui-dock-size", id));
|
||||
if ui.data(|d| d.get_temp::<[f32; 2]>(told)) != Some(dock) && dock[0] >= 1.0 && dock[1] >= 1.0 {
|
||||
ui.data_mut(|d| d.insert_temp(told, dock));
|
||||
acts.push(Act::Resize(id, dock));
|
||||
}
|
||||
let content = rect;
|
||||
egui::ScrollArea::both().id_salt(("scriptui-panel", id)).max_width(content.width()).max_height(content.height()).show(ui, |ui| {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(w.root.bounds[2] as f32, w.root.bounds[3] as f32), Sense::hover());
|
||||
|
||||
@@ -117,15 +117,20 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value
|
||||
ui.add_sized(vec2(label_w, 18.0), egui::Label::new(format!("{}:", tr(label))).truncate());
|
||||
let r = match kind {
|
||||
Kind::Int(lo, hi, unit) => {
|
||||
let mut x = v.as_i64().unwrap_or(0);
|
||||
// A typed number applies once entered: typing 85 into UI Scale
|
||||
// doesn't resize the interface to 75 % at its first digit.
|
||||
// A dragged value applies on release, and a typed one once
|
||||
// entered: UI Scale doesn't resize the interface under the
|
||||
// pointer mid-drag (#2582) or at a typed number's first digit.
|
||||
let held = egui::Id::unique(("settings-drag", key));
|
||||
let mut x = ui.data(|d| d.get_temp::<i64>(held)).unwrap_or_else(|| v.as_i64().unwrap_or(0));
|
||||
let drag = egui::DragValue::new(&mut x).range(lo..=hi).speed(0.25).update_while_editing(false);
|
||||
let r = ui.add(crate::widgets::drag_value(drag));
|
||||
if !unit.is_empty() {
|
||||
ui.label(RichText::new(tr(unit)).color(t.text_dim));
|
||||
}
|
||||
if r.changed() {
|
||||
if r.dragged() {
|
||||
ui.data_mut(|d| d.insert_temp(held, x));
|
||||
} else if r.changed() || r.drag_stopped() {
|
||||
ui.data_mut(|d| d.remove::<i64>(held));
|
||||
acts.push(Act::Set(key.into(), json!(x)));
|
||||
}
|
||||
r
|
||||
|
||||
@@ -664,3 +664,33 @@ fn transition_presets_apply_from_effects_and_presets() {
|
||||
assert_eq!(fx.name, "Radial Wipe");
|
||||
assert_eq!(fx.prop("completion").unwrap().keys.len(), 2);
|
||||
}
|
||||
|
||||
/// #2582: dragging Settings' UI Scale doesn't resize the interface under the pointer; the value
|
||||
/// applies once the button is released.
|
||||
#[test]
|
||||
fn ui_scale_applies_when_the_drag_ends() {
|
||||
let mut h = harness();
|
||||
open_settings(&mut h);
|
||||
let e = h.state().auto.find("settings.appearance.uiScale").expect("the UI Scale field");
|
||||
let from = pos2(e.rect[0] + e.rect[2] / 2.0, e.rect[1] + e.rect[3] / 2.0);
|
||||
h.input_mut().events.push(Event::PointerMoved(from));
|
||||
h.input_mut().events.push(Event::PointerButton { pos: from, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
|
||||
h.step();
|
||||
for k in 1..=8 {
|
||||
h.input_mut().events.push(Event::PointerMoved(from + egui::vec2(10.0 * k as f32, 0.0)));
|
||||
h.step();
|
||||
assert_eq!(scale(&h), (100, 1.0), "nothing applies mid-drag");
|
||||
}
|
||||
h.input_mut().events.push(Event::PointerButton {
|
||||
pos: from + egui::vec2(80.0, 0.0),
|
||||
button: egui::PointerButton::Primary,
|
||||
pressed: false,
|
||||
modifiers: Default::default(),
|
||||
});
|
||||
h.run_steps(4);
|
||||
let (pct, zoom) = scale(&h);
|
||||
assert!(pct > 100 && (zoom - pct as f32 / 100.0).abs() < 1e-6, "applied on release: {pct} {zoom}");
|
||||
click(&mut h, "settings.cancel");
|
||||
h.run_steps(2);
|
||||
assert_eq!(scale(&h), (100, 1.0));
|
||||
}
|
||||
|
||||
@@ -341,6 +341,38 @@ fn a_drag_keeps_its_control_past_the_window_edge() {
|
||||
h.state_mut().session.execute("scriptui.close", json!({})).unwrap();
|
||||
}
|
||||
|
||||
/// #2585: a window the host resizes takes the size, lays out again and runs onResize; a docked
|
||||
/// ScriptUI panel gets its dock's size that way.
|
||||
#[test]
|
||||
fn script_windows_learn_the_size_they_are_given() {
|
||||
let mut h = harness();
|
||||
let code = r#"
|
||||
var w = new Window("palette", "Sized");
|
||||
var log = w.add("statictext", undefined, "", { name: "log" });
|
||||
log.preferredSize = [200, 20];
|
||||
w.onResize = function () { log.text = "resized " + w.size.width + "x" + w.size.height; };
|
||||
w.show();
|
||||
"#;
|
||||
let r = h.state_mut().session.execute("script.run", json!({"code": code, "name": "sized.jsx"})).unwrap();
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.run_steps(2);
|
||||
h.state_mut().session.execute("scriptui.resize", json!({"window": "Sized", "size": [300, 180]})).unwrap();
|
||||
let win = h.state().session.script_ui.windows[0].clone();
|
||||
assert_eq!(win.root.lookup(&json!("log")).unwrap().text, "resized 300x180");
|
||||
let list = h.state_mut().session.execute("scriptui.list", json!({})).unwrap();
|
||||
assert_eq!(list[0]["size"], json!([300.0, 180.0]), "{list}");
|
||||
assert!(h.state_mut().session.execute("scriptui.resize", json!({"window": "Sized", "size": ["wide", 1]})).is_err());
|
||||
h.state_mut().session.execute("scriptui.close", json!({})).unwrap();
|
||||
// A docked panel: its window is its dock's size, not its preferred size.
|
||||
h.state_mut().session.execute("window.scriptPanel", json!({"name": "Layer Tools.jsx"})).unwrap();
|
||||
h.run_steps(4);
|
||||
let panel = h.state().session.script_ui.windows.iter().find(|w| w.script == "Layer Tools.jsx").unwrap().clone();
|
||||
let dock = rect(&h, &format!("scriptui.{}", panel.id));
|
||||
assert_eq!([panel.root.bounds[2].round(), panel.root.bounds[3].round()], [dock.width().round() as f64, dock.height().round() as f64]);
|
||||
h.state_mut().close_panel(PanelKind::ScriptPanel(panel.id));
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
fn click_at(h: &mut Harness<'_, EffectcraftApp>, c: egui::Pos2) {
|
||||
h.input_mut().events.push(Event::PointerMoved(c));
|
||||
h.run_steps(1);
|
||||
|
||||
@@ -20,7 +20,7 @@ our own scripts; no real-world script library has been run against both apps.
|
||||
| Expression performance | Cached, multi-frame | Per-thread boa contexts | unknown | No benchmark |
|
||||
| Scripting object model | `app`, `Project`, items, layers, properties, keys, eases, render queue, `TextDocument`, `Shape`, markers, `File`/`Folder`, `Socket`, `app.settings` | Same classes, edits run through undoable engine commands; match names table; layers, groups and properties are callable (`layer("Effects")(1)` is `.property(...)`) | good | `crates/script` tests (≈ 10 test files) |
|
||||
| `$.evalFile`, `BridgeTalk` | Evaluate another script file; identify the host application and message other Adobe applications | `$.evalFile` evaluates a file relative to the calling file (nested at most 32 deep, under the scripts' file gate) and returns its last value; `BridgeTalk` names the host honestly (`appName` `effectcraft`, its version); messaging other applications throws "not supported" | partial | `tests_evalfile.rs` (#760); no inter-application messaging |
|
||||
| ScriptUI | Dialogs, palettes, dockable panels, resource strings, `onDraw` / ScriptUIGraphics, mouse events | Same, dockable panels reopen at launch | good | `ui_scriptui`, `tests_ui*` |
|
||||
| ScriptUI | Dialogs, palettes, dockable panels, resource strings, `onDraw` / ScriptUIGraphics, mouse events | Same, dockable panels reopen at launch; a docked panel gets its dock's size and `onResize` (`scriptui.resize`, #2585); a drag keeps reaching the control it started on past the window edge, and `MouseEvent.buttons` reports the held buttons (#2584) | good | `ui_scriptui`, `tests_ui*` |
|
||||
| `app.scheduleTask` / `cancelTask` | One-shot and repeating tasks (polling, panel refresh) until `cancelTask` | Same: repeating tasks run every `delay` ms (at least 50 ms, at most 64 pending per script) until `cancelTask`, until they throw (reported once, then cancelled), or until their owner goes: a script's last window closing, or the project closing for scripts without windows (a Startup script's tasks go on until EffectCraft quits); frontends tick them every frame, agents with `script.runTasks` | good | `tests_tasks.rs` (#773); one-shot tasks of a script file still run right after it, not after their delay |
|
||||
| Scripts menu, ScriptUI Panels folder | yes | yes; a subfolder (one level) of either folder is a submenu named after it | same | [plugins.md](plugins.md) |
|
||||
| Startup / Shutdown folders | yes | `Scripts/Startup` runs at launch and `Scripts/Shutdown` runs as the desktop app quits, after the project is closed, each in alphabetical order | partial | `crates/script/src/tests/startup.rs`; each script has its own global scope, so a Startup script's variables and functions are not available to later scripts ([plugins.md](plugins.md)) |
|
||||
|
||||
Reference in New Issue
Block a user