mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 10:17:02 -06:00
M3.86: Alt-click the mask thumbnail to view only the mask
transparency.viewOpacityMask {on?, id?} shows only the opacity mask on the
canvas as greyscale coverage (luminance, clip and invert applied; the rest of
the art hidden) and enters mask editing; called again it shows the artwork and
keeps editing. The Transparency panel's mask thumbnail runs it on Alt-click and
is outlined in the accent colour while the mask is viewed. The state is
DocState.mask_view (shown while that mask is edited: DocState::shown_mask) and
reaches the renderer as RenderOptions.mask_view.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
cbdef4ea1f
commit
03571affbd
@@ -145,6 +145,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let painter = ui.painter_at(rect);
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let doc = st.doc.clone();
|
||||
let mask_view = st.shown_mask();
|
||||
|
||||
// Pasteboard, artboard shadows and paper.
|
||||
painter.rect_filled(rect, 0.0, t.pasteboard);
|
||||
@@ -217,6 +218,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let [r, g, b, _] = c.to_rgba8(1.0);
|
||||
[r, g, b]
|
||||
}),
|
||||
mask_view,
|
||||
..opts
|
||||
};
|
||||
// Light documents render synchronously (no lag vs overlays); heavy ones go to the worker.
|
||||
|
||||
@@ -439,6 +439,8 @@ mod tests_effectedit;
|
||||
#[cfg(test)]
|
||||
mod tests_freeform;
|
||||
#[cfg(test)]
|
||||
mod tests_maskview;
|
||||
#[cfg(test)]
|
||||
mod tests_stroke;
|
||||
#[cfg(test)]
|
||||
mod tests_strokedepth;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
//! Headless frames of the Transparency panel's mask thumbnail (M3.86): Alt-click views only the
|
||||
//! mask (and edits it), Alt-click again shows the artwork.
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// One frame of the Transparency panel with `events`, `modifiers` held.
|
||||
fn frame(ctx: &egui::Context, app: &mut VectorcraftApp, events: Vec<egui::Event>, modifiers: egui::Modifiers) {
|
||||
let events = std::iter::once(egui::Event::ModifiersChanged(modifiers)).chain(events).collect();
|
||||
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| transparency::show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
}
|
||||
|
||||
/// Click the mask thumbnail with `modifiers` held.
|
||||
fn click_mask(ctx: &egui::Context, app: &mut VectorcraftApp, modifiers: egui::Modifiers) {
|
||||
frame(ctx, app, vec![], egui::Modifiers::NONE);
|
||||
let at = ctx.read_response(egui::Id::new(transparency::MASK_THUMB)).expect("the mask thumbnail is drawn").rect.center();
|
||||
let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers };
|
||||
frame(ctx, app, vec![egui::Event::PointerMoved(at), button(true), button(false)], modifiers);
|
||||
frame(ctx, app, vec![], egui::Modifiers::NONE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alt_clicking_the_mask_thumbnail_views_only_the_mask() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
let run = |app: &mut VectorcraftApp, id: &str, p: serde_json::Value| app.session.execute(id, &p).unwrap();
|
||||
run(&mut app, "file.new", json!({"width": 100, "height": 100}));
|
||||
let a = run(&mut app, "shape.rectangle", json!({"x": 0, "y": 0, "width": 50, "height": 50}))["id"].as_u64().unwrap();
|
||||
let b = run(&mut app, "shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20}))["id"].clone();
|
||||
run(&mut app, "select.set", json!({"ids": [a, b]}));
|
||||
run(&mut app, "transparency.makeOpacityMask", json!({}));
|
||||
let ctx = egui::Context::default();
|
||||
let viewed = |app: &VectorcraftApp| app.session.active().unwrap().shown_mask().map(|n| n.0);
|
||||
click_mask(&ctx, &mut app, egui::Modifiers::ALT);
|
||||
assert_eq!(viewed(&app), Some(a));
|
||||
assert!(app.session.active().unwrap().doc.mask_edit.is_some(), "viewing the mask edits it");
|
||||
click_mask(&ctx, &mut app, egui::Modifiers::ALT);
|
||||
assert_eq!(viewed(&app), None);
|
||||
assert!(app.session.active().unwrap().doc.mask_edit.is_some(), "still editing");
|
||||
// A plain click while editing does nothing; Alt views again.
|
||||
click_mask(&ctx, &mut app, egui::Modifiers::NONE);
|
||||
assert_eq!(viewed(&app), None);
|
||||
click_mask(&ctx, &mut app, egui::Modifiers::ALT);
|
||||
assert_eq!(viewed(&app), Some(a));
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
//! Transparency panel: blend mode, opacity (field + slider popup), object/mask thumbnails with the
|
||||
//! opacity-mask controls (make/release, link, clip, invert; Shift-click the mask to disable it),
|
||||
//! Isolate Blending, the three-state Knockout Group (on → neutral → off) and Opacity & Mask Define
|
||||
//! Knockout Shape; the menu's Page Isolated Blending and Page Knockout Group.
|
||||
//! opacity-mask controls (make/release, link, clip, invert; Shift-click the mask to disable it,
|
||||
//! Alt-click it to view only the mask), Isolate Blending, the three-state Knockout Group (on →
|
||||
//! neutral → off) and Opacity & Mask Define Knockout Shape; the menu's Page Isolated Blending and
|
||||
//! Page Knockout Group.
|
||||
|
||||
use egui::{Sense, Stroke, StrokeKind, Ui, vec2};
|
||||
use serde_json::json;
|
||||
@@ -14,6 +15,9 @@ use crate::theme::Tokens;
|
||||
use crate::widgets::{self, TransparencyEdit, menu_item};
|
||||
use crate::{VectorcraftApp, icons};
|
||||
|
||||
/// Interaction id of the mask thumbnail.
|
||||
pub(super) const MASK_THUMB: &str = "tr-mask-thumb";
|
||||
|
||||
/// The panel's values ([`vectorcraft_engine::Session::transparency_info`]) and its first target
|
||||
/// (the selection, or the object whose mask is being edited), recomputed only when the document
|
||||
/// or the selection changes.
|
||||
@@ -100,25 +104,31 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if lresp.on_hover_text(if m.linked { "Unlink the mask" } else { "Link the mask" }).clicked() {
|
||||
app.run("transparency.setOpacityMask", json!({"linked": !m.linked})).ok();
|
||||
}
|
||||
let (mr, mresp) = ui.allocate_exact_size(vec2(50.0, 50.0), Sense::click());
|
||||
let (mr, _) = ui.allocate_exact_size(vec2(50.0, 50.0), Sense::hover());
|
||||
let mresp = ui.interact(mr, egui::Id::new(MASK_THUMB), Sense::click());
|
||||
let bg = if m.clip != m.invert { [0, 0, 0, 255] } else { [255, 255, 255, 255] };
|
||||
ui.painter().rect_filled(mr, 0.0, egui::Color32::from_rgb(bg[0], bg[1], bg[2]));
|
||||
node_thumb(app, ui, "tr-mask", &m.art, mr.shrink(2.0), Some(bg));
|
||||
ui.painter().rect_stroke(
|
||||
mr,
|
||||
0.0,
|
||||
Stroke::new(mask_w, if editing.is_some() { t.border } else { t.input_border }),
|
||||
StrokeKind::Inside,
|
||||
);
|
||||
// Viewing the mask alone (Alt-click) highlights its thumbnail.
|
||||
let viewing = app.session.active().is_some_and(|d| d.shown_mask().is_some());
|
||||
let border = match (viewing, editing.is_some()) {
|
||||
(true, _) => Stroke::new(2.0, t.accent),
|
||||
(false, true) => Stroke::new(mask_w, t.border),
|
||||
(false, false) => Stroke::new(mask_w, t.input_border),
|
||||
};
|
||||
ui.painter().rect_stroke(mr, 0.0, border, StrokeKind::Inside);
|
||||
if m.disabled {
|
||||
let red = Stroke::new(2.0, egui::Color32::from_rgb(220, 40, 40));
|
||||
ui.painter().line_segment([mr.left_top(), mr.right_bottom()], red);
|
||||
ui.painter().line_segment([mr.right_top(), mr.left_bottom()], red);
|
||||
}
|
||||
let shift = ui.input(|i| i.modifiers.shift);
|
||||
if mresp.on_hover_text("Click to edit the mask; Shift-click to disable or enable it").clicked() {
|
||||
let (shift, alt) = ui.input(|i| (i.modifiers.shift, i.modifiers.alt));
|
||||
let tip = "Click to edit the mask; Alt-click to view only the mask; Shift-click to disable or enable it";
|
||||
if mresp.on_hover_text(tip).clicked() {
|
||||
if shift {
|
||||
app.run(if m.disabled { "transparency.enableOpacityMask" } else { "transparency.disableOpacityMask" }, json!({})).ok();
|
||||
} else if alt {
|
||||
app.run("transparency.viewOpacityMask", json!({"id": id.0})).ok();
|
||||
} else if editing.is_none() {
|
||||
app.run("transparency.editOpacityMask", json!({"id": id.0})).ok();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user