mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Merge pull request #430 from bobtherobot/fix/appearance-opacity-popup
Appearance: the Opacity popup's blend list and slider work
This commit is contained in:
5 files changed
+66
-13
No files matched your search
+2
-2
@@ -4,7 +4,7 @@ VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adob
|
||||
|
||||
This file tracks **how far we are and what's left**. Time estimates are wall-clock hours of continuous Claude Opus 5.5 agent work (including builds and the CI gate), given both for **one agent** and for **4–6 parallel agents** on disjoint crates. They are counted from the remaining work (see [Parity estimate](#parity-estimate)), calibrated against measured throughput, and updated as work lands.
|
||||
|
||||
_Last updated: 2026-10-08 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426); see [Honest assessment](#honest-assessment-2026-10-05))._
|
||||
_Last updated: 2026-10-08 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430); see [Honest assessment](#honest-assessment-2026-10-05))._
|
||||
|
||||
## Where we are
|
||||
|
||||
@@ -141,7 +141,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||
- **Colour:** a Color Picker (colour field, H/S/B/R/G/B channel slider, HSB/RGB/Lab/CMYK fields, hex, web-only snapping, out-of-gamut correction); the Color panel follows each colour's own model and Alt-click paints the other proxy; proxies look through groups and show "?" for mixed paint; Invert, Complement, Apply Last Color (`,`) and Apply Last Gradient (`.`) keep the colour model; every paint command feeds the recent colours. The Color Guide has 21 harmony rules, Steps and Variation options and Limit to Library. Adjust Color Balance (its Global mode shifts tints) and Saturate preview live; Edit Colors and Recolor also reach meshes, images and patterns. Recolor Artwork has colour reduction, five recolour methods, presets, Limit to Library, a colour-wheel Edit tab and Edit Color Group; a local Color Themes panel saves five-colour themes. Fill/Stroke chips open swatch and mixer popovers, panels have their shortcuts, and Eyedropper Options pick up and apply chosen attributes (Alt and Shift+Alt, image pixels). Fills and strokes can overprint (Attributes panel, Overprint Black options), and CMYK documents blend in inks on screen, in PDF and when flattening. Document Color Mode converts the art, symbols, pattern tiles and swatches through the colour settings: Gray stays Gray, RGB greys separate into four-colour greys and a rich black as Illustrator does, or go on the black plate only with `grays: "black"` (also for `document.open {colorMode}`).
|
||||
- **Gradients:** lossless, validated gradient params; gradients follow rotate, reflect, shear, non-uniform scale and distortions; the Gradient tool edits the fill or stroke of the active proxy, type included; an interactive on-canvas annotator (move, length/angle, rotate, add/move/delete/duplicate stops, midpoints, a stop popover, Delete and arrow keys). The Gradient panel has the Fill/Stroke proxy, a gradient-swatch dropdown with Save to Swatches, midpoint locations, Alt-drag to copy or swap stops, swatch drops on the ramp and a stop eyedropper; copied appearances and graphic styles place gradients on each target's own bounds; gradient handles snap and honour Constrain Angle. Gradient stops stay linked to global and spot swatches. Freeform gradients (points and lines) are edited on the canvas; radial gradients have an extent ellipse, an aspect handle and an off-centre focal point (SVG `fx`/`fy`, PDF two-point radials). A stroke's gradient runs within, along or across it. Object ▸ Expand turns gradients into N objects or a gradient mesh.
|
||||
- **Strokes:** one stroke-geometry module shared by the canvas, SVG, PDF and Outline Stroke; dotted lines (zero-length dashes draw dots or squares); arrowheads with hollow outlines, tip-on-end or extend-past-end alignment and one opacity for line and head. SVG and PDF export arrowheads, width profiles, brushes and aligned strokes exactly as the canvas draws them; Outline Stroke and the live Outline Stroke effect outline every stroke item as drawn (dashes, heads, profiles, alignment); dashes can be fitted to corners and path ends; width profiles carry through dashes, and their corners take the join. The Stroke panel edits type's character strokes, follows the stroke units (its weight presets are round values in that unit: `0.25 mm`, `0.0625 in`, `1 px`, #426; fields show four decimals in mm, cm and inches, three in points), shows mixed values blank and opens from the Control bar. A width profile library, thirty more arrowheads, and Width Point Edit with multi-select and discontinuous points. Visual bounds and clicks cover the whole painted stroke. Scale Strokes & Effects, Scale Corners and Use Preview Bounds work everywhere, and new art inherits stroke options, a graphic style or the whole last appearance.
|
||||
- **Appearance:** the Appearance panel's selected row drives every paint, stroke, gradient and transparency edit (`item` on the commands); effects apply to one fill or stroke; per-item Opacity popups; Mixed Appearances, Layers target dots and basic-appearance rules. Effects can be reordered, moved between items and copied by dragging; Show All Hidden Attributes; clicking an effect edits it in place; the object thumbnail drags onto art; effects render on type, images, symbols, blends, envelopes, meshes and repeats, and per-item raster effects export. Groups, layers and type have their own fills, strokes and effects (Contents and Characters rows); the Layers panel's target circles target layers, groups and objects; Expand Appearance turns fills, strokes, effects and brushes into objects.
|
||||
- **Appearance:** the Appearance panel's selected row drives every paint, stroke, gradient and transparency edit (`item` on the commands); effects apply to one fill or stroke; per-item Opacity popups, whose blend-mode list and opacity slider stay open inside them (#430; the Control bar's Stroke popover too); Mixed Appearances, Layers target dots and basic-appearance rules. Effects can be reordered, moved between items and copied by dragging; Show All Hidden Attributes; clicking an effect edits it in place; the object thumbnail drags onto art; effects render on type, images, symbols, blends, envelopes, meshes and repeats, and per-item raster effects export. Groups, layers and type have their own fills, strokes and effects (Contents and Characters rows); the Layers panel's target circles target layers, groups and objects; Expand Appearance turns fills, strokes, effects and brushes into objects.
|
||||
- **Masks and clipping:** opacity-mask controls work while the mask is being edited; transparency commands take ids and work without a selection; clipping sets clip by compound paths, even-odd paths, text and groups on screen and in raster export, matching SVG/PDF. Alt-click shows only the mask; SVG masks keep Clip and Invert; the transparency grid is per-document state; a clipping path's own fill and stroke paint behind and over the clipped art; layers can be clipping masks.
|
||||
- **Transparency:** three-state Knockout Group (on, neutral, off), Opacity & Mask Define Knockout Shape, Page Isolated Blending and Page Knockout Group, on screen and in SVG/PDF. Blend modes match the standard formulas on screen, in SVG and in PDF, and non-isolated groups blend with the art below. Flatten Transparency with presets, a dialog and a Flattener Preview panel.
|
||||
- **Graphic styles:** styles keep opacity and blend mode, capture groups and type, apply on top with Alt, and link to the objects using them; Redefine, Break Link, Graphic Style Options, Select All Unused, Sort by Name, and Select > Same Graphic Style / Appearance Attribute. The panel has list and thumbnail views with rendered previews, merge, reorder and drag and drop, plus a Control bar Style picker; six generated style libraries save and load as `.vcstyles`.
|
||||
|
||||
@@ -668,16 +668,13 @@ fn opacity_row(app: &mut VectorcraftApp, ui: &mut Ui, item: Option<usize>, opaci
|
||||
let lx = r.left() + EYE_W + 24.0 + if item.is_some() { 12.0 } else { 0.0 };
|
||||
let clicked = link(ui, pos2(lx, r.center().y), ("ap-op-link", item), tl!("Opacity:")) || resp.clicked();
|
||||
text(ui, pos2(lx + 56.0, r.center().y), &opacity_text(opacity, blend), false);
|
||||
egui::Popup::menu(&resp)
|
||||
.open_memory(clicked.then_some(egui::SetOpenCommand::Toggle))
|
||||
.close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
|
||||
.show(|ui| match widgets::opacity_blend(ui, ("ap-op", item), Some(opacity), Some(blend), true) {
|
||||
Some(TransparencyEdit::Blend(b)) => {
|
||||
app.run("transparency.set", json!({"item": item, "blend": b.label()})).ok();
|
||||
}
|
||||
Some(TransparencyEdit::Opacity(o, phase)) => live_run(app, "Opacity", "transparency.set", json!({"item": item, "opacity": o}), phase),
|
||||
None => {}
|
||||
});
|
||||
widgets::popover(&resp, clicked, |ui| match widgets::opacity_blend(ui, ("ap-op", item), Some(opacity), Some(blend), true) {
|
||||
Some(TransparencyEdit::Blend(b)) => {
|
||||
app.run("transparency.set", json!({"item": item, "blend": b.label()})).ok();
|
||||
}
|
||||
Some(TransparencyEdit::Opacity(o, phase)) => live_run(app, "Opacity", "transparency.set", json!({"item": item, "opacity": o}), phase),
|
||||
None => {}
|
||||
});
|
||||
r
|
||||
}
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ pub(crate) fn weight_field(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, weig
|
||||
/// The Stroke panel in a popover anchored to `resp` (the Control bar's and the Properties
|
||||
/// panel's Stroke links), which a click on it toggles.
|
||||
pub(crate) fn popover(app: &mut VectorcraftApp, resp: &egui::Response) {
|
||||
egui::Popup::menu(resp).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| {
|
||||
widgets::popover(resp, resp.clicked(), |ui| {
|
||||
ui.set_width(260.0);
|
||||
show(app, ui);
|
||||
});
|
||||
|
||||
@@ -168,6 +168,29 @@ fn item_opacity_popup_sets_the_items_opacity() {
|
||||
assert_eq!((n.appearance.items[0].opacity(), n.opacity), (1.0, 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opacity_popup_blend_dropdown_sets_the_blend_mode() {
|
||||
let mut app = app_with_rect();
|
||||
let ctx = egui::Context::default();
|
||||
let texts = frame_events(&ctx, &mut app, vec![], appearance::show);
|
||||
// The object's own Opacity row (the last one).
|
||||
let link = texts.iter().rev().find(|(t, _)| t == "Opacity:").expect("object opacity row").1;
|
||||
click(&ctx, &mut app, link.center(), appearance::show);
|
||||
let texts = frame_events(&ctx, &mut app, vec![], appearance::show);
|
||||
// Opening the dropdown inside the popup keeps the popup open: the list shows.
|
||||
click(&ctx, &mut app, text_rect(&texts, "Normal").center(), appearance::show);
|
||||
let texts = frame_events(&ctx, &mut app, vec![], appearance::show);
|
||||
click(&ctx, &mut app, text_rect(&texts, "Multiply").center(), appearance::show);
|
||||
assert_eq!(first_selected(&app).unwrap().blend, vectorcraft_color::BlendMode::Multiply);
|
||||
// The popup is still open after choosing.
|
||||
let texts = frame_events(&ctx, &mut app, vec![], appearance::show);
|
||||
assert!(texts.iter().any(|(t, _)| t == "Multiply"), "{texts:?}");
|
||||
// A frame without the panel closes it, as egui closes the popups it remembers.
|
||||
frame_events(&ctx, &mut app, vec![], |_, _| {});
|
||||
let texts = frame_events(&ctx, &mut app, vec![], appearance::show);
|
||||
assert!(!texts.iter().any(|(t, _)| t == "Multiply"), "{texts:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_row_names_the_selection_like_the_control_bar() {
|
||||
let mut app = app_with_rect();
|
||||
|
||||
@@ -559,6 +559,39 @@ pub(crate) fn combo<R>(
|
||||
.inner
|
||||
}
|
||||
|
||||
/// A popover anchored to `resp`, opened and closed by `toggle` (usually `resp.clicked()`), and
|
||||
/// closed by Escape, a click outside it and outside any popup it opened, or a frame in which its
|
||||
/// anchor isn't shown. Use it instead of `egui::Popup::menu` when the popover holds dropdowns or
|
||||
/// popups of its own: egui remembers one open popup at a time, so opening a dropdown inside a
|
||||
/// remembered popup would close the popup (and the dropdown with it). This one keeps its open
|
||||
/// state itself: the last frame it was open in.
|
||||
pub fn popover<R>(resp: &Response, toggle: bool, content: impl FnOnce(&mut Ui) -> R) -> Option<R> {
|
||||
let ctx = &resp.ctx;
|
||||
let id = resp.id.with("popover");
|
||||
let frame = ctx.cumulative_frame_nr();
|
||||
let was_open = ctx.data(|d| d.get_temp::<u64>(id)).is_some_and(|f| f + 1 >= frame);
|
||||
let mut open = if toggle {
|
||||
!was_open
|
||||
} else {
|
||||
// Popups (this one and those it opened) are on foreground layers; a click anywhere
|
||||
// else closes it.
|
||||
let clicked_elsewhere = ctx
|
||||
.input(|i| i.pointer.any_click().then(|| i.pointer.interact_pos()).flatten())
|
||||
.is_some_and(|pos| ctx.layer_id_at(pos).is_none_or(|l| l.order != egui::Order::Foreground));
|
||||
was_open && !clicked_elsewhere
|
||||
};
|
||||
let inner =
|
||||
egui::Popup::menu(resp).id(id).open_bool(&mut open).close_behavior(egui::PopupCloseBehavior::IgnoreClicks).show(content).map(|r| r.inner);
|
||||
ctx.data_mut(|d| {
|
||||
if open {
|
||||
d.insert_temp(id, frame);
|
||||
} else {
|
||||
d.remove::<u64>(id);
|
||||
}
|
||||
});
|
||||
inner
|
||||
}
|
||||
|
||||
/// The body of a menu or popup list: as tall as its items up to the bottom of the window, and
|
||||
/// scrolling only past that, so long menus (Window, Effect, a panel's menu) stay reachable on
|
||||
/// small windows.
|
||||
|
||||
Reference in new issue
Block a user