Merge pull request #183 from storytold/fix/147-156-162-viewer
Viewer: Pan Behind snapping and modifiers, locked viewers keep their comp (#147, #156, #162)
This commit is contained in:
commit
b09f06f417
|
|
@ -6,7 +6,7 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use effectcraft_color::{ColorSpace, space};
|
||||
use effectcraft_geom::vec2;
|
||||
use effectcraft_geom::{Mat3, vec2};
|
||||
use effectcraft_project::{Layer, LayerId};
|
||||
use effectcraft_raster::Image;
|
||||
use effectcraft_render::EvalCtx;
|
||||
|
|
@ -131,23 +131,32 @@ pub fn snap(sources: &[[f64; 2]], targets: &[SnapTarget], tol: f64) -> Option<Sn
|
|||
/// Snap features of a layer in comp space: corners, edge midpoints, centre and anchor point
|
||||
/// (2D layers; `None` for layers without bounds).
|
||||
pub fn layer_features(ctx: &EvalCtx, layer: &Layer) -> Vec<[f64; 2]> {
|
||||
let mut out = vec![];
|
||||
let (m, _) = ctx.layer_to_comp(layer);
|
||||
if let Some(b) = effectcraft_render::content_bounds(ctx, layer) {
|
||||
let (cx, cy) = ((b[0] + b[2]) / 2.0, (b[1] + b[3]) / 2.0);
|
||||
for p in [[b[0], b[1]], [b[2], b[1]], [b[2], b[3]], [b[0], b[3]], [cx, b[1]], [b[2], cy], [cx, b[3]], [b[0], cy], [cx, cy]] {
|
||||
let q = m.apply(vec2(p[0], p[1]));
|
||||
out.push([q.x, q.y]);
|
||||
}
|
||||
}
|
||||
if let Some(tr) = layer.transform() {
|
||||
let a = ctx.v3(layer, tr, "anchor", [0.0; 3]);
|
||||
let q = m.apply(vec2(a[0], a[1]));
|
||||
out.push([q.x, q.y]);
|
||||
}
|
||||
let mut out = box_features(ctx, layer, &m);
|
||||
out.extend(anchor_feature(ctx, layer, &m));
|
||||
out
|
||||
}
|
||||
|
||||
/// The corners, edge midpoints and centre of a layer's content box (`m`: layer → comp).
|
||||
fn box_features(ctx: &EvalCtx, layer: &Layer, m: &Mat3) -> Vec<[f64; 2]> {
|
||||
let Some(b) = effectcraft_render::content_bounds(ctx, layer) else { return vec![] };
|
||||
let (cx, cy) = ((b[0] + b[2]) / 2.0, (b[1] + b[3]) / 2.0);
|
||||
[[b[0], b[1]], [b[2], b[1]], [b[2], b[3]], [b[0], b[3]], [cx, b[1]], [b[2], cy], [cx, b[3]], [b[0], cy], [cx, cy]]
|
||||
.into_iter()
|
||||
.map(|p| {
|
||||
let q = m.apply(vec2(p[0], p[1]));
|
||||
[q.x, q.y]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A layer's anchor point (`m`: layer → comp).
|
||||
fn anchor_feature(ctx: &EvalCtx, layer: &Layer, m: &Mat3) -> Option<[f64; 2]> {
|
||||
let a = ctx.v3(layer, layer.transform()?, "anchor", [0.0; 3]);
|
||||
let q = m.apply(vec2(a[0], a[1]));
|
||||
Some([q.x, q.y])
|
||||
}
|
||||
|
||||
/// Mask and shape-path vertices of a layer in comp space.
|
||||
pub fn layer_vertices(ctx: &EvalCtx, layer: &Layer) -> Vec<[f64; 2]> {
|
||||
let (m, _) = ctx.layer_to_comp(layer);
|
||||
|
|
@ -163,7 +172,7 @@ pub fn layer_vertices(ctx: &EvalCtx, layer: &Layer) -> Vec<[f64; 2]> {
|
|||
}
|
||||
}
|
||||
for (uid, sp) in shape_paths(ctx, layer) {
|
||||
let pm = m * effectcraft_render::shapes::item_matrix(ctx, layer, uid).unwrap_or(effectcraft_geom::Mat3::IDENTITY);
|
||||
let pm = m * effectcraft_render::shapes::item_matrix(ctx, layer, uid).unwrap_or(Mat3::IDENTITY);
|
||||
out.extend(sp.vertices.iter().map(|v| {
|
||||
let q = pm.apply(vec2(v[0], v[1]));
|
||||
[q.x, q.y]
|
||||
|
|
@ -214,19 +223,8 @@ pub fn targets(ctx: &EvalCtx, exclude: &[LayerId], opts: SnapOptions) -> Vec<Sna
|
|||
SnapTarget::hline(h / 2.0, PRI_COMP, SnapSource::Comp),
|
||||
SnapTarget::point([w / 2.0, h / 2.0], PRI_COMP, SnapSource::Comp),
|
||||
];
|
||||
for l in comp.layers.iter().filter(|l| l.is_active_at(ctx.time) && !exclude.contains(&l.id) && !l.is_camera() && !l.is_light() && !l.is_3d()) {
|
||||
let f = layer_features(ctx, l);
|
||||
// Edges of unrotated layers are lines too.
|
||||
if f.len() >= 9 && (f[0][1] - f[1][1]).abs() < 1e-6 && (f[0][0] - f[3][0]).abs() < 1e-6 {
|
||||
for x in [f[0][0], f[1][0]] {
|
||||
out.push(SnapTarget { kind: SnapKind::VLine, pos: [x, f[0][1]], priority: PRI_LAYER, source: SnapSource::Layer(l.id) });
|
||||
}
|
||||
for y in [f[0][1], f[3][1]] {
|
||||
out.push(SnapTarget { kind: SnapKind::HLine, pos: [f[0][0], y], priority: PRI_LAYER, source: SnapSource::Layer(l.id) });
|
||||
}
|
||||
}
|
||||
out.extend(f.into_iter().map(|p| SnapTarget::point(p, PRI_LAYER, SnapSource::Layer(l.id))));
|
||||
out.extend(layer_vertices(ctx, l).into_iter().map(|p| SnapTarget::point(p, PRI_VERTEX, SnapSource::Vertex(l.id))));
|
||||
for l in comp.layers.iter().filter(|l| l.is_active_at(ctx.time) && !exclude.contains(&l.id)) {
|
||||
out.extend(layer_targets(ctx, l, true));
|
||||
}
|
||||
if opts.guides {
|
||||
for g in &comp.guides {
|
||||
|
|
@ -253,6 +251,33 @@ pub fn targets(ctx: &EvalCtx, exclude: &[LayerId], opts: SnapOptions) -> Vec<Sna
|
|||
out
|
||||
}
|
||||
|
||||
/// A 2D layer's snap targets: its box points (edges as lines too while unrotated), its anchor
|
||||
/// point unless `anchor` is false (Pan Behind drags it, the box stays put) and its mask and shape
|
||||
/// path vertices. None for cameras, lights and 3D layers.
|
||||
pub fn layer_targets(ctx: &EvalCtx, l: &Layer, anchor: bool) -> Vec<SnapTarget> {
|
||||
if l.is_camera() || l.is_light() || l.is_3d() {
|
||||
return vec![];
|
||||
}
|
||||
let (m, _) = ctx.layer_to_comp(l);
|
||||
let f = box_features(ctx, l, &m);
|
||||
let mut out = vec![];
|
||||
if let [nw, ne, _, sw, ..] = f.as_slice()
|
||||
&& (nw[1] - ne[1]).abs() < 1e-6
|
||||
&& (nw[0] - sw[0]).abs() < 1e-6
|
||||
{
|
||||
for x in [nw[0], ne[0]] {
|
||||
out.push(SnapTarget { kind: SnapKind::VLine, pos: [x, nw[1]], priority: PRI_LAYER, source: SnapSource::Layer(l.id) });
|
||||
}
|
||||
for y in [nw[1], sw[1]] {
|
||||
out.push(SnapTarget { kind: SnapKind::HLine, pos: [nw[0], y], priority: PRI_LAYER, source: SnapSource::Layer(l.id) });
|
||||
}
|
||||
}
|
||||
let a = if anchor { anchor_feature(ctx, l, &m) } else { None };
|
||||
out.extend(f.into_iter().chain(a).map(|p| SnapTarget::point(p, PRI_LAYER, SnapSource::Layer(l.id))));
|
||||
out.extend(layer_vertices(ctx, l).into_iter().map(|p| SnapTarget::point(p, PRI_VERTEX, SnapSource::Vertex(l.id))));
|
||||
out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- channels and exposure
|
||||
|
||||
/// Show Channel and Color Management Settings.
|
||||
|
|
|
|||
|
|
@ -62,6 +62,14 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
if resp.clicked() {
|
||||
app.ui.tool = cur;
|
||||
}
|
||||
// Ctrl+double-click Pan Behind: Center Anchor Point in Layer Content.
|
||||
if cur == Tool::PanBehind
|
||||
&& resp.double_clicked()
|
||||
&& ui.input(|i| i.modifiers.command)
|
||||
&& let Err(e) = crate::menus::invoke(app, ui.ctx(), "layer.centerAnchor", json!({}))
|
||||
{
|
||||
app.ui.status = e;
|
||||
}
|
||||
if slot.len() > 1 {
|
||||
resp.context_menu(|ui| {
|
||||
for tool in slot.iter() {
|
||||
|
|
|
|||
|
|
@ -93,6 +93,10 @@ enum Gesture {
|
|||
start: [f64; 2],
|
||||
inv: Mat3,
|
||||
l2p: Mat3,
|
||||
/// The layer's own snap targets as the drag began (its box and path vertices).
|
||||
own: Vec<effectcraft_engine::viewer::SnapTarget>,
|
||||
/// Alt held as the drag began: the anchor point moves alone, so the layer shifts.
|
||||
anchor_only: bool,
|
||||
},
|
||||
Pan {
|
||||
start_pan: [f32; 2],
|
||||
|
|
@ -274,6 +278,17 @@ fn drag_scale(start_scale: [f64; 3], start: [f64; 2], cur: [f64; 2], axes: [bool
|
|||
[start_scale[0] * fx, start_scale[1] * fy, start_scale[2]]
|
||||
}
|
||||
|
||||
/// A drag `d` (comp pixels), kept to the axis it moves along most when `shift` is held.
|
||||
fn one_axis(d: [f64; 2], shift: bool) -> [f64; 2] {
|
||||
if !shift {
|
||||
d
|
||||
} else if d[0].abs() > d[1].abs() {
|
||||
[d[0], 0.0]
|
||||
} else {
|
||||
[0.0, d[1]]
|
||||
}
|
||||
}
|
||||
|
||||
fn point_in_quad(p: [f64; 2], q: &[[f64; 2]; 4]) -> bool {
|
||||
let mut sign = 0.0;
|
||||
for i in 0..4 {
|
||||
|
|
@ -1057,6 +1072,8 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
start: map.to_comp(press),
|
||||
inv: l2c.inverse().unwrap_or(Mat3::IDENTITY),
|
||||
l2p,
|
||||
own: effectcraft_engine::viewer::layer_targets(&ectx, layer, false),
|
||||
anchor_only: mods.alt,
|
||||
})
|
||||
}),
|
||||
t if t.is_shape() => Some(Gesture::Create { tool: t, start: cpt }),
|
||||
|
|
@ -1198,10 +1215,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
}
|
||||
}
|
||||
Gesture::Move { layers, start, snap_src } => {
|
||||
let mut d = [cpt[0] - start[0], cpt[1] - start[1]];
|
||||
if mods.shift {
|
||||
if d[0].abs() > d[1].abs() { d[1] = 0.0 } else { d[0] = 0.0 }
|
||||
}
|
||||
let mut d = one_axis([cpt[0] - start[0], cpt[1] - start[1]], mods.shift);
|
||||
if let Some(src) = snap_src {
|
||||
let ids: Vec<LayerId> = layers.iter().map(|x| x.0).collect();
|
||||
let c = vt::snap(app, &ctx, &ectx, &map, &ids, &[[src[0] + d[0], src[1] + d[1]]], mods);
|
||||
|
|
@ -1225,20 +1239,24 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
}
|
||||
let _ = app.session.execute("prop.set", json!({"layer": layer.0, "path": "transform/rotation", "value": r, "merge": merge}));
|
||||
}
|
||||
Gesture::Anchor { layer, start_anchor, start_pos, start, inv, l2p } => {
|
||||
// Pan Behind snaps the anchor point (to layer features, guides and the grid).
|
||||
Gesture::Anchor { layer, start_anchor, start_pos, start, inv, l2p, own, anchor_only } => {
|
||||
// Pan Behind snaps the anchor point (to its own layer's box, other layers' features,
|
||||
// guides and the grid); Shift keeps the move to one axis.
|
||||
let d = one_axis([cpt[0] - start[0], cpt[1] - start[1]], mods.shift);
|
||||
let anchor_c = inv.inverse().map(|m| m.apply(gv2(start_anchor[0], start_anchor[1]))).unwrap_or(gv2(start[0], start[1]));
|
||||
let want = [anchor_c.x + cpt[0] - start[0], anchor_c.y + cpt[1] - start[1]];
|
||||
let sc = vt::snap(app, &ctx, &ectx, &map, &[layer], &[want], mods);
|
||||
let cpt = [cpt[0] + sc[0], cpt[1] + sc[1]];
|
||||
let want = [anchor_c.x + d[0], anchor_c.y + d[1]];
|
||||
let sc = vt::snap_with(app, &ctx, &ectx, &map, &[layer], &[want], mods, &own);
|
||||
let a = inv.apply(gv2(start[0], start[1]));
|
||||
let b = inv.apply(gv2(cpt[0], cpt[1]));
|
||||
let b = inv.apply(gv2(start[0] + d[0] + sc[0], start[1] + d[1] + sc[1]));
|
||||
let dl = gv2(b.x - a.x, b.y - a.y);
|
||||
let na = [start_anchor[0] + dl.x, start_anchor[1] + dl.y, start_anchor[2]];
|
||||
let dp = l2p.apply_vec(dl);
|
||||
let np = [start_pos[0] + dp.x, start_pos[1] + dp.y, start_pos[2]];
|
||||
let _ = app.session.execute("prop.set", json!({"layer": layer.0, "path": "transform/anchor", "value": na, "merge": merge}));
|
||||
let _ = app.session.execute("prop.set", json!({"layer": layer.0, "path": "transform/position", "value": np, "merge": merge}));
|
||||
// Position follows so the layer stays put, unless Alt moves the anchor point alone.
|
||||
if !anchor_only {
|
||||
let dp = l2p.apply_vec(dl);
|
||||
let np = [start_pos[0] + dp.x, start_pos[1] + dp.y, start_pos[2]];
|
||||
let _ = app.session.execute("prop.set", json!({"layer": layer.0, "path": "transform/position", "value": np, "merge": merge}));
|
||||
}
|
||||
}
|
||||
Gesture::Camera { tool } => {
|
||||
let d = resp.drag_delta();
|
||||
|
|
|
|||
|
|
@ -45,6 +45,20 @@ pub(crate) fn snap(
|
|||
exclude: &[LayerId],
|
||||
sources: &[[f64; 2]],
|
||||
mods: egui::Modifiers,
|
||||
) -> [f64; 2] {
|
||||
snap_with(app, ctx, ectx, map, exclude, sources, mods, &[])
|
||||
}
|
||||
|
||||
/// [`snap`] with `extra` layer targets (the Pan Behind layer's own box).
|
||||
pub(crate) fn snap_with(
|
||||
app: &EffectcraftApp,
|
||||
ctx: &egui::Context,
|
||||
ectx: &EvalCtx,
|
||||
map: &ViewerMap,
|
||||
exclude: &[LayerId],
|
||||
sources: &[[f64; 2]],
|
||||
mods: egui::Modifiers,
|
||||
extra: &[vw::SnapTarget],
|
||||
) -> [f64; 2] {
|
||||
let layers = snapping_on(app, mods);
|
||||
let v = &app.ui.viewer;
|
||||
|
|
@ -55,6 +69,7 @@ pub(crate) fn snap(
|
|||
}
|
||||
let opts = vw::SnapOptions { guides, grid, grid_spacing: app.session.prefs.grids.grid_spacing };
|
||||
let mut targets = vw::targets(ectx, exclude, opts);
|
||||
targets.extend_from_slice(extra);
|
||||
if !layers {
|
||||
targets.retain(|t| matches!(t.source, SnapSource::Guide | SnapSource::Grid));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,8 +73,14 @@ pub fn comp_of(app: &EffectcraftApp, id: u32) -> Option<ItemId> {
|
|||
/// becomes the active viewer when clicked.
|
||||
pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: u32, rect: Rect) {
|
||||
if id == app.ui.active_viewer {
|
||||
let c = app.session.active_comp_id().map(|c| c.0);
|
||||
app.ui.viewers.insert(id, c);
|
||||
// A comp opened earlier this frame (the Project panel) is routed next frame by
|
||||
// `on_open_comp`, which needs the comp this viewer showed (to keep it when locked).
|
||||
if app.session.events.iter().any(|e| matches!(e, effectcraft_engine::Event::OpenComp(_))) {
|
||||
ui.ctx().request_repaint();
|
||||
} else {
|
||||
let c = app.session.active_comp_id().map(|c| c.0);
|
||||
app.ui.viewers.insert(id, c);
|
||||
}
|
||||
super::viewer::show(app, ui, rect);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -440,23 +440,101 @@ fn pan_behind_snaps_the_anchor_point() {
|
|||
h.run_steps(2);
|
||||
// Drag the anchor (at the box centre) to 3 px from the comp centre: it snaps there, and the
|
||||
// position follows so the box doesn't move.
|
||||
let from = screen(&h, [100.0, 100.0]);
|
||||
let to = screen(&h, [323.0, 182.0]);
|
||||
// (The gesture applies the pointer of the previous frame: hold the end point a frame.)
|
||||
let (from, to) = (screen(&h, [100.0, 100.0]), screen(&h, [323.0, 182.0]));
|
||||
hold_drag(&mut h, from, to, Default::default());
|
||||
let (p, a) = position_and_anchor(&h, box_id);
|
||||
assert!((p[0] - 320.0).abs() < 0.01 && (p[1] - 180.0).abs() < 0.01, "anchor point snapped to the comp centre: {p:?}");
|
||||
assert!((a[0] - 260.0).abs() < 0.01 && (a[1] - 120.0).abs() < 0.01, "{a:?}");
|
||||
}
|
||||
|
||||
/// Issue #147: Pan Behind snaps the anchor to its own layer's corners, edge midpoints and centre
|
||||
/// (only the anchor itself is left out: the box stays put while the anchor moves).
|
||||
#[test]
|
||||
fn pan_behind_snaps_the_anchor_to_its_own_layer() {
|
||||
let mut h = harness();
|
||||
let box_id = h.state().session.active_comp().unwrap().layers[0].id;
|
||||
h.state_mut().session.execute("prop.set", json!({"layer": box_id.0, "path": "transform/position", "value": [100, 100, 0]})).unwrap();
|
||||
h.state_mut().session.execute("layer.select", json!({"layers": [box_id.0]})).unwrap();
|
||||
h.state_mut().ui.tool = Tool::PanBehind;
|
||||
h.run_steps(2);
|
||||
// The 80×80 box spans (60, 60)–(140, 140). Its anchor, dragged to 3 px from the top-left
|
||||
// corner, lands on it.
|
||||
let (from, to) = (screen(&h, [100.0, 100.0]), screen(&h, [63.0, 62.0]));
|
||||
hold_drag(&mut h, from, to, Default::default());
|
||||
let (p, a) = position_and_anchor(&h, box_id);
|
||||
assert!(a[0].abs() < 0.01 && a[1].abs() < 0.01, "anchor point snapped to the corner: {a:?}");
|
||||
assert!((p[0] - 60.0).abs() < 0.01 && (p[1] - 60.0).abs() < 0.01, "the box stays put: {p:?}");
|
||||
// And back to near the centre: it snaps there.
|
||||
let (from, to) = (screen(&h, [61.0, 61.0]), screen(&h, [103.0, 98.0]));
|
||||
hold_drag(&mut h, from, to, Default::default());
|
||||
let (p, a) = position_and_anchor(&h, box_id);
|
||||
assert!((a[0] - 40.0).abs() < 0.01 && (a[1] - 40.0).abs() < 0.01, "anchor point snapped to the centre: {a:?}");
|
||||
assert!((p[0] - 100.0).abs() < 0.01 && (p[1] - 100.0).abs() < 0.01, "{p:?}");
|
||||
}
|
||||
|
||||
/// Issue #162: with Pan Behind, Alt-drag moves the anchor point alone (the layer shifts, its
|
||||
/// position stays), Shift keeps the move to one axis, and Ctrl+double-clicking the tool's button
|
||||
/// centres the anchor point in the layer content.
|
||||
#[test]
|
||||
fn pan_behind_alt_moves_the_anchor_alone_shift_constrains_and_the_tool_button_centres_it() {
|
||||
let mut h = harness();
|
||||
let box_id = h.state().session.active_comp().unwrap().layers[0].id;
|
||||
h.state_mut().session.execute("prop.set", json!({"layer": box_id.0, "path": "transform/position", "value": [100, 100, 0]})).unwrap();
|
||||
h.state_mut().session.execute("layer.select", json!({"layers": [box_id.0]})).unwrap();
|
||||
h.state_mut().session.execute("view.snapping", json!({"value": false})).unwrap();
|
||||
h.state_mut().ui.tool = Tool::PanBehind;
|
||||
h.run_steps(2);
|
||||
let close = |a: [f64; 3], b: [f64; 2]| (a[0] - b[0]).abs() < 0.01 && (a[1] - b[1]).abs() < 0.01;
|
||||
// Alt: the anchor point moves by the drag, Position stays.
|
||||
let (from, to) = (screen(&h, [100.0, 100.0]), screen(&h, [120.0, 105.0]));
|
||||
hold_drag(&mut h, from, to, egui::Modifiers::ALT);
|
||||
let (p, a) = position_and_anchor(&h, box_id);
|
||||
assert!(close(a, [60.0, 45.0]) && close(p, [100.0, 100.0]), "Alt moves the anchor point alone: {a:?} {p:?}");
|
||||
// Shift: the move keeps to the axis dragged along most (Position compensates).
|
||||
h.state_mut().session.execute("prop.set", json!({"layer": box_id.0, "path": "transform/anchor", "value": [40, 40, 0]})).unwrap();
|
||||
h.run_steps(2);
|
||||
let (from, to) = (screen(&h, [100.0, 100.0]), screen(&h, [130.0, 110.0]));
|
||||
hold_drag(&mut h, from, to, egui::Modifiers::SHIFT);
|
||||
let (p, a) = position_and_anchor(&h, box_id);
|
||||
assert!(close(a, [70.0, 40.0]) && close(p, [130.0, 100.0]), "Shift keeps to x: {a:?} {p:?}");
|
||||
// Ctrl+double-click the tool's button: Center Anchor Point in Layer Content (the layer stays).
|
||||
let ctrl = egui::Modifiers { ctrl: true, command: true, ..Default::default() };
|
||||
let b = rect(&h, "tools.PanBehind").center();
|
||||
h.input_mut().events.push(Event::ModifiersChanged(ctrl));
|
||||
h.input_mut().events.push(Event::PointerMoved(b));
|
||||
h.step();
|
||||
for _ in 0..2 {
|
||||
h.input_mut().events.push(Event::PointerButton { pos: b, button: egui::PointerButton::Primary, pressed: true, modifiers: ctrl });
|
||||
h.input_mut().events.push(Event::PointerButton { pos: b, button: egui::PointerButton::Primary, pressed: false, modifiers: ctrl });
|
||||
}
|
||||
h.run_steps(2);
|
||||
h.input_mut().events.push(Event::ModifiersChanged(Default::default()));
|
||||
h.step();
|
||||
let (p, a) = position_and_anchor(&h, box_id);
|
||||
assert!(close(a, [40.0, 40.0]) && close(p, [100.0, 100.0]), "anchor point centred: {a:?} {p:?}");
|
||||
}
|
||||
|
||||
/// Drag from `from` to `to` with `modifiers` held, holding the end point a frame (the viewer's
|
||||
/// gestures apply the pointer of the previous frame).
|
||||
fn hold_drag(h: &mut Harness<'_, EffectcraftApp>, from: Pos2, to: Pos2, modifiers: egui::Modifiers) {
|
||||
h.input_mut().events.push(Event::ModifiersChanged(modifiers));
|
||||
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.input_mut().events.push(Event::PointerButton { pos: from, button: egui::PointerButton::Primary, pressed: true, modifiers });
|
||||
h.step();
|
||||
for i in 1..=10 {
|
||||
h.input_mut().events.push(Event::PointerMoved(from + (to - from) * (i.min(8) as f32 / 8.0)));
|
||||
h.step();
|
||||
}
|
||||
h.input_mut().events.push(Event::PointerButton { pos: to, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() });
|
||||
h.input_mut().events.push(Event::PointerButton { pos: to, button: egui::PointerButton::Primary, pressed: false, modifiers });
|
||||
h.run_steps(2);
|
||||
let l = h.state().session.active_comp().unwrap().layer(box_id).unwrap().clone();
|
||||
let p = l.props.prop("transform/position").unwrap().value.as_vec3();
|
||||
let a = l.props.prop("transform/anchor").unwrap().value.as_vec3();
|
||||
assert!((p[0] - 320.0).abs() < 0.01 && (p[1] - 180.0).abs() < 0.01, "anchor point snapped to the comp centre: {p:?}");
|
||||
assert!((a[0] - 260.0).abs() < 0.01 && (a[1] - 120.0).abs() < 0.01, "{a:?}");
|
||||
h.input_mut().events.push(Event::ModifiersChanged(Default::default()));
|
||||
h.step();
|
||||
}
|
||||
|
||||
/// A layer's Position and Anchor Point.
|
||||
fn position_and_anchor(h: &Harness<'_, EffectcraftApp>, layer: LayerId) -> ([f64; 3], [f64; 3]) {
|
||||
let l = h.state().session.active_comp().unwrap().layer(layer).unwrap().clone();
|
||||
(l.props.prop("transform/position").unwrap().value.as_vec3(), l.props.prop("transform/anchor").unwrap().value.as_vec3())
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -17,13 +17,35 @@ fn invoke(h: &mut Harness<'_, EffectcraftApp>, id: &str, p: Value) {
|
|||
h.run_steps(3);
|
||||
}
|
||||
|
||||
fn click(h: &mut Harness<'_, EffectcraftApp>, id: &str) {
|
||||
/// Hover element `id`; returns its centre.
|
||||
fn hover(h: &mut Harness<'_, EffectcraftApp>, id: &str) -> egui::Pos2 {
|
||||
let e = h.state().auto.find(id).unwrap_or_else(|| panic!("no {id}")).clone();
|
||||
let p = pos2(e.rect[0] + e.rect[2] / 2.0, e.rect[1] + e.rect[3] / 2.0);
|
||||
h.input_mut().events.push(Event::PointerMoved(p));
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
|
||||
h.input_mut().events.push(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() });
|
||||
p
|
||||
}
|
||||
|
||||
fn button(h: &mut Harness<'_, EffectcraftApp>, pos: egui::Pos2, pressed: bool) {
|
||||
h.input_mut().events.push(Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() });
|
||||
}
|
||||
|
||||
/// Click element `id`, the release a frame after the press (as a hand does: a press in a tab
|
||||
/// strip focuses the shown panel first, the click then the tab).
|
||||
fn click(h: &mut Harness<'_, EffectcraftApp>, id: &str) {
|
||||
let p = hover(h, id);
|
||||
button(h, p, true);
|
||||
h.step();
|
||||
button(h, p, false);
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
/// Double-click element `id` (in one frame: the harness' frame time is longer than a double-click's).
|
||||
fn double_click(h: &mut Harness<'_, EffectcraftApp>, id: &str) {
|
||||
let p = hover(h, id);
|
||||
for pressed in [true, false, true, false] {
|
||||
button(h, p, pressed);
|
||||
}
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
|
|
@ -31,14 +53,16 @@ fn shows(h: &Harness<'_, EffectcraftApp>, viewer: u32) -> Option<u64> {
|
|||
viewers::comp_of(h.state(), viewer).map(|c| c.0)
|
||||
}
|
||||
|
||||
fn new_comp(s: &mut Session, name: &str) -> u64 {
|
||||
s.execute("comp.new", json!({"name": name, "width": 64, "height": 36, "duration": 1})).unwrap()["comp"].as_u64().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_viewers_lock_route_comps_and_activate_on_click() {
|
||||
let mut s = Session::default();
|
||||
let new =
|
||||
|s: &mut Session, name: &str| s.execute("comp.new", json!({"name": name, "width": 64, "height": 36, "duration": 1})).unwrap()["comp"].as_u64().unwrap();
|
||||
let c = new(&mut s, "C");
|
||||
let b = new(&mut s, "B");
|
||||
let a = new(&mut s, "A");
|
||||
let c = new_comp(&mut s, "C");
|
||||
let b = new_comp(&mut s, "B");
|
||||
let a = new_comp(&mut s, "A");
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
|
||||
h.run_steps(3);
|
||||
// View ▸ New Viewer: viewer 1 shows A too, viewer 0 (the Composition panel) is locked.
|
||||
|
|
@ -87,6 +111,29 @@ fn new_viewers_lock_route_comps_and_activate_on_click() {
|
|||
assert!(titles.iter().any(|t| t == "Composition A") && titles.iter().any(|t| t == "Composition C"), "{titles:?}");
|
||||
}
|
||||
|
||||
/// Issue #156: a locked viewer keeps its comp when the Project panel opens another one. The
|
||||
/// Project panel opens it mid-frame, before the viewers draw; the active (locked) viewer used to
|
||||
/// record the new comp as its own then, so the lock no longer applied.
|
||||
#[test]
|
||||
fn a_locked_viewer_keeps_its_comp_when_the_project_panel_opens_another() {
|
||||
let mut s = Session::default();
|
||||
let b = new_comp(&mut s, "B");
|
||||
let a = new_comp(&mut s, "A");
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
|
||||
h.run_steps(3);
|
||||
// Viewer 0 locked to A, and B in viewer 1 (a tab next to it).
|
||||
invoke(&mut h, "view.newViewer", json!({}));
|
||||
invoke(&mut h, "comp.open", json!({"comp": b}));
|
||||
// Back to the locked viewer by its tab.
|
||||
click(&mut h, "panel.tab.Composition");
|
||||
assert!(viewers::locked(h.state(), 0));
|
||||
assert_eq!((h.state().ui.active_viewer, shows(&h, 0), shows(&h, 1)), (0, Some(a), Some(b)));
|
||||
// Double-click B in the Project panel: it shows in its viewer, the locked one keeps A.
|
||||
double_click(&mut h, &format!("project.item.{b}.name"));
|
||||
assert_eq!(h.state().session.active_comp_id(), Some(ItemId(b)));
|
||||
assert_eq!((h.state().ui.active_viewer, shows(&h, 0), shows(&h, 1)), (1, Some(a), Some(b)));
|
||||
}
|
||||
|
||||
/// `cargo test -p effectcraft-ui-egui --test ui_viewers -- --ignored`: two viewers side by side
|
||||
/// (`target/test-out/viewers.png`).
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in New Issue