From dbc595b0d6cd37db886fc0b34057c8360b6c4f1a Mon Sep 17 00:00:00 2001 From: Jannik Wappler <167129173+RavoxX@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:38:06 +0300 Subject: [PATCH] #1220: Hide layer bounds during path editing --- crates/ui-egui/src/panels/viewer.rs | 8 +++ crates/ui-egui/tests/ui_viewer.rs | 97 +++++++++++++++++++++++++++++ docs/gaps.md | 6 +- docs/ui-parity.md | 9 ++- 4 files changed, 118 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/panels/viewer.rs b/crates/ui-egui/src/panels/viewer.rs index f4bb5a88..6ec3be36 100644 --- a/crates/ui-egui/src/panels/viewer.rs +++ b/crates/ui-egui/src/panels/viewer.rs @@ -1028,6 +1028,14 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { } } } + // Path editing keeps vertices and tangents visible without the layer's transform + // box competing with them (#1220). The path's own free-transform box is separate. + let editing_path = paths.iter().any(|p| { + p.layer == l.id && (app.ui.tool.is_pen() || (app.ui.tool == Tool::Selection && sel_vertices.iter().any(|v| v.layer == l.id && v.mask == p.uid))) + }); + if editing_path { + continue; + } let Some((m, q, _)) = layer_quad(&ectx, l) else { // A 3D layer reaching past the camera: the part of its box in front of it. let outline = layer_outline(&ectx, l); diff --git a/crates/ui-egui/tests/ui_viewer.rs b/crates/ui-egui/tests/ui_viewer.rs index e60226b1..d37180a8 100644 --- a/crates/ui-egui/tests/ui_viewer.rs +++ b/crates/ui-egui/tests/ui_viewer.rs @@ -677,6 +677,103 @@ fn fill_options_change_the_selected_shape_layer() { } } +/// #1220: path tools hide the layer transform box, while path vertices remain editable. +#[test] +fn path_editing_hides_layer_bounds_and_keeps_corner_vertices_editable() { + for mask in [false, true] { + let mut h = harness(); + let vertices = [[200.0, 100.0], [400.0, 100.0], [400.0, 260.0], [200.0, 260.0]]; + let (layer, uid, kind) = if mask { + let layer = layer_id(&h, "Plate"); + h.state_mut().session.execute("layer.select", json!({"layers": [layer]})).unwrap(); + let r = h + .state_mut() + .session + .execute("mask.new", json!({"layer": layer, "vertices": vertices, "outTangents": [[40, 0], [0, 0], [0, 0], [0, 0]], "closed": true})) + .unwrap(); + (layer, r["mask"].as_u64().unwrap(), "mask") + } else { + let r = h.state_mut().session.execute("shape.newPath", json!({"vertices": vertices, "outTangents": [[40, 0], [0, 0], [0, 0], [0, 0]], "closed": true, "fill": false, "stroke": "#ffffff", "strokeWidth": 4})).unwrap(); + (r["layer"].as_u64().unwrap(), r["path"].as_u64().unwrap(), "shapePath") + }; + h.state_mut().session.state.selected_vertices.clear(); + h.run_steps(2); + let handle = format!("viewer.handle.{layer}.0"); + assert!(h.state().auto.find(&handle).is_some(), "ordinary layer selection has transform handles (mask={mask})"); + for tool in [Tool::Pen, Tool::PenAdd, Tool::PenDelete, Tool::PenConvert, Tool::MaskFeather] { + h.state_mut().ui.tool = tool; + h.run_steps(2); + assert!(h.state().auto.find(&handle).is_none(), "{tool:?}: hide the layer box"); + assert!(h.state().auto.find(&format!("viewer.{kind}.{uid}.vertex.0")).is_some(), "{tool:?}: keep the path point"); + } + h.state_mut().ui.tool = Tool::Selection; + h.run_steps(2); + let vertex = format!("viewer.{kind}.{uid}.vertex.0"); + let from = rect(&h, &vertex).center(); + click(&mut h, from); + h.run_steps(2); // The automation registry also keeps the preceding frame's elements. + assert!(h.state().auto.find(&handle).is_none(), "selecting a vertex starts path editing (mask={mask})"); + let position = h.state().session.active_comp().unwrap().layer(LayerId(layer)).unwrap().props.prop("transform/position").unwrap().value.clone(); + let path = |h: &Harness<'_, EffectcraftApp>| { + h.state() + .session + .active_comp() + .unwrap() + .layer(LayerId(layer)) + .unwrap() + .props + .find_group(uid) + .unwrap() + .get("path") + .unwrap() + .value + .as_path() + .unwrap() + .clone() + }; + let before = path(&h); + drag(&mut h, from, from + vec2(15.0, 12.0)); + assert_ne!(path(&h).vertices[0], before.vertices[0], "the corner vertex can still be moved"); + assert_eq!(path(&h).vertices[1..], before.vertices[1..]); + assert_eq!(h.state().session.active_comp().unwrap().layer(LayerId(layer)).unwrap().props.prop("transform/position").unwrap().value, position); + h.state_mut().session.execute("edit.undo", json!({})).unwrap(); + h.run_steps(2); + assert_eq!(path(&h), before, "moving the vertex undoes in one step"); + let z = rect(&h, "viewer.comp").width() / 640.0; + let tangent = rect(&h, &vertex).center() + vec2(40.0 * z, 0.0); + drag(&mut h, tangent, tangent + vec2(0.0, 20.0)); + assert_ne!(path(&h).out_tangents[0], before.out_tangents[0], "the Bézier handle is still editable"); + assert_eq!(path(&h).vertices, before.vertices, "a tangent drag does not move the path points"); + h.state_mut().session.execute("edit.undo", json!({})).unwrap(); + h.run_steps(2); + assert_eq!(path(&h), before, "the tangent drag undoes in one step"); + h.state_mut().ui.tool = Tool::Rotate; + h.run_steps(2); + assert!(h.state().auto.find(&handle).is_some(), "a layer transform tool restores the box"); + h.state_mut().ui.tool = Tool::Selection; + h.state_mut().session.state.selected_vertices.clear(); + h.run_steps(2); + assert!(h.state().auto.find(&handle).is_some(), "ending vertex selection restores the box"); + } +} + +#[test] +fn path_tools_keep_bounds_on_layers_without_visible_paths() { + let mut h = harness(); + let layer = layer_id(&h, "Box"); + h.state_mut().session.execute("layer.select", json!({"layers": [layer]})).unwrap(); + h.state_mut().ui.tool = Tool::Pen; + h.run_steps(2); + let handle = format!("viewer.handle.{layer}.0"); + assert!(h.state().auto.find(&handle).is_some(), "a layer without a path keeps its box"); + h.state_mut().session.execute("mask.new", json!({"layer": layer, "vertices": [[0, 0], [80, 0], [80, 80]], "closed": true})).unwrap(); + h.run_steps(2); + assert!(h.state().auto.find(&handle).is_none()); + h.state_mut().ui.viewer.show_masks = false; + h.run_steps(2); + assert!(h.state().auto.find(&handle).is_some(), "hidden paths do not suppress the layer controls"); +} + #[test] fn shape_pen_draws_a_closed_shape_layer() { let mut h = harness(); diff --git a/docs/gaps.md b/docs/gaps.md index c22a97fb..0c5e3094 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where EffectCraft falls short -> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (issue, PR and catalog references gap 21: percentage guides and effect colour labels in; earlier change: re-audited against `upstream/main` 470ea7a3; ranked gap list from the 10 October re-measure otherwise unchanged) · **Target:** Adobe After Effects 2026 (26.5.0.89) +> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-13 · **Change:** minor (path-editing layer bounds, #1220; issue, PR and catalog references gap 21: percentage guides and effect colour labels in; earlier change: re-audited against `upstream/main` 470ea7a3; ranked gap list from the 10 October re-measure otherwise unchanged) · **Target:** Adobe After Effects 2026 (26.5.0.89) An honest assessment of how far EffectCraft is from being a real replacement for After Effects, and the work that closes the gap. This document is meant for contributors and agents choosing @@ -114,6 +114,9 @@ The most important missing piece: it turns every other estimate here into a meas - Run the headless snapshot and control-channel checks on Linux (X11 and Wayland) and Windows, not only macOS. Interactions that only fail with real input (docking drags, viewer pan, drag-and-drop import, inline rename) need scripted input tests. +- Path editing (#1220): selected shape and mask paths suppress the layer transform box while + their points are edited. `ui_viewer` regressions cover Pen-family tools, Selection vertex + dragging and undo, restoration of layer controls, and layers without visible paths. - Regression evidence for [#66](https://github.com/storytold/effectcraft/issues/66): scripted pointer tests in `ui_viewer` and `ui_text_edit`, checked headlessly on Linux, cover viewer clicks, marquee selection and text picking when layers are hidden or excluded by solo. They also check @@ -322,6 +325,7 @@ Issues #41–#47 (all from the Linux AppImage 0.1.1, 5 October): panning the vie | Date | Change | Summary | |---|---|---| +| 2026-10-13 | patch | G2: layer bounds no longer compete with editable shape/mask path points (#1220) | | 2026-10-12 | minor | Audited against `upstream/main` 470ea7a3: open-issue count and crash list, closed issues removed from rows 1, 3, 5, 6, 8–12, 15–17, 22–24, G8 and G9 (#759 `#include`), localization catalogs recounted (≈ 12% to ≈ 18%); rows 7, 14 and 21 left to open PRs #1102, #792 and #1098 | | 2026-10-10 | minor | Alpha gate checked: no alpha blockers | | 2026-10-12 | minor | Gap 21: percentage guides and effect colour labels added (#1095) | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 22da668d..366d8f77 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI parity -> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (closed issue references re-audited against `upstream/main` 470ea7a3) · **Target:** Adobe After Effects 2026 +> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-13 · **Change:** minor (path-editing layer bounds, #1220; closed issue references re-audited against `upstream/main` 470ea7a3) · **Target:** Adobe After Effects 2026 How closely EffectCraft's interaction matches After Effects: tools, handles, snapping, nudging, modifier keys, shortcuts, drag behaviour, numeric entry, panels, context menus and feel. Evidence @@ -33,6 +33,12 @@ feel and platform differences, and the 26.x interface changes are not in yet. | Accessibility | AccessKit enabled; focus labels tested; Project panel keyboard navigation and accessible names (#776) | 20% | No screen-reader pass or keyboard-navigation audit of the other panels; no contrast check | | UI scale | Settings ▸ Appearance ▸ UI Scale 75–200% | beyond | After Effects follows the system's scaling (#284) | +Path editing (#1220): the Composition viewer suppresses a selected layer’s transform box while +using a Pen-family tool on a visible path, or while selecting its vertices with the Selection +tool. Path points, Bézier handles and the path’s free-transform controls remain available. +Layer transform tools and ordinary layer selection restore the box. Headless `ui_viewer` tests +cover shape paths and masks, corner-vertex dragging and undo, and layers without visible paths. + ## Estimate **≈ 60–120 h**: the open interaction issues above (25–45 h, about 0.2–0.5 h each with a scripted @@ -44,6 +50,7 @@ Effects running; most of the rest parallelises. | Date | Change | Summary | |---|---|---| +| 2026-10-13 | patch | Layer bounds are suppressed during path-point editing (#1220); shape/mask interaction regressions | | 2026-10-12 | minor | Closed issues (#302, #320, #491, #511, #553 and others) marked as closed instead of open; panels in their own windows (#486) | | 2026-10-12 | minor | Percentage guides and effect colour labels (26.5) added; rows updated | | 2026-10-10 | major | First UI parity checklist |